diff --git a/slack_bolt/adapter/socket_mode/async_internals.py b/slack_bolt/adapter/socket_mode/async_internals.py index c2965f766..00c33bf58 100644 --- a/slack_bolt/adapter/socket_mode/async_internals.py +++ b/slack_bolt/adapter/socket_mode/async_internals.py @@ -8,13 +8,14 @@ from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.socket_mode.response import SocketModeResponse +from slack_bolt.adapter.socket_mode.internals import build_retry_headers from slack_bolt.app.async_app import AsyncApp from slack_bolt.request.async_request import AsyncBoltRequest from slack_bolt.response import BoltResponse async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest): - bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload) + bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload, headers=build_retry_headers(req)) bolt_resp: BoltResponse = await app.async_dispatch(bolt_req) return bolt_resp diff --git a/slack_bolt/adapter/socket_mode/internals.py b/slack_bolt/adapter/socket_mode/internals.py index 8eb751b4d..9d6c3f898 100644 --- a/slack_bolt/adapter/socket_mode/internals.py +++ b/slack_bolt/adapter/socket_mode/internals.py @@ -3,6 +3,7 @@ import json import logging from time import time +from typing import Dict, Optional, Sequence, Union from slack_sdk.socket_mode.client import BaseSocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest @@ -13,8 +14,18 @@ from slack_bolt.response import BoltResponse +def build_retry_headers(req: SocketModeRequest) -> Optional[Dict[str, Union[str, Sequence[str]]]]: + # Mirror the HTTP mode retry headers so middleware/listeners can detect Events API retries + headers: Dict[str, Union[str, Sequence[str]]] = {} + if req.retry_attempt is not None: + headers["x-slack-retry-num"] = str(req.retry_attempt) + if req.retry_reason is not None: + headers["x-slack-retry-reason"] = req.retry_reason + return headers or None + + def run_bolt_app(app: App, req: SocketModeRequest): - bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload) + bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload, headers=build_retry_headers(req)) bolt_resp: BoltResponse = app.dispatch(bolt_req) return bolt_resp diff --git a/tests/adapter_tests/socket_mode/test_internals.py b/tests/adapter_tests/socket_mode/test_internals.py new file mode 100644 index 000000000..2289196d4 --- /dev/null +++ b/tests/adapter_tests/socket_mode/test_internals.py @@ -0,0 +1,39 @@ +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_bolt.adapter.socket_mode.internals import build_retry_headers, run_bolt_app + + +class TestSocketModeInternals: + def test_build_retry_headers_without_retry(self): + req = SocketModeRequest(type="events_api", envelope_id="e1", payload={"type": "event_callback"}) + assert build_retry_headers(req) is None + + def test_build_retry_headers_with_retry(self): + req = SocketModeRequest( + type="events_api", + envelope_id="e1", + payload={"type": "event_callback"}, + retry_attempt=2, + retry_reason="http_timeout", + ) + headers = build_retry_headers(req) + assert headers == {"x-slack-retry-num": "2", "x-slack-retry-reason": "http_timeout"} + + def test_run_bolt_app_propagates_retry_headers(self): + captured = {} + + class FakeApp: + def dispatch(self, bolt_req): + captured["headers"] = bolt_req.headers + return None + + req = SocketModeRequest( + type="events_api", + envelope_id="e1", + payload={"type": "event_callback", "event": {"type": "app_mention"}}, + retry_attempt=1, + retry_reason="http_timeout", + ) + run_bolt_app(FakeApp(), req) + assert captured["headers"]["x-slack-retry-num"] == ["1"] + assert captured["headers"]["x-slack-retry-reason"] == ["http_timeout"]