From 1e4939772b9e9cb7d40f1ebf9d3f501685c70cb2 Mon Sep 17 00:00:00 2001 From: Nahid Date: Mon, 6 Oct 2025 07:54:57 +0600 Subject: [PATCH] Add an easier way to access X-Slack-Retry-Num / X-Slack-Retry-Reason in Bolt apps --- slack_bolt/adapter/__init__.py | 3 +-- .../adapter/socket_mode/async_internals.py | 9 ++++++- slack_bolt/adapter/socket_mode/internals.py | 9 ++++++- slack_bolt/context/base_context.py | 12 ++++++++++ slack_bolt/request/async_internals.py | 16 ++++++++++++- slack_bolt/request/async_request.py | 2 +- slack_bolt/request/internals.py | 17 ++++++++++++- slack_bolt/request/request.py | 2 +- tests/scenario_tests/test_events.py | 22 +++++++++++++++++ .../scenario_tests/test_events_socket_mode.py | 24 +++++++++++++++++++ 10 files changed, 108 insertions(+), 8 deletions(-) diff --git a/slack_bolt/adapter/__init__.py b/slack_bolt/adapter/__init__.py index f339226bc..9ca556e52 100644 --- a/slack_bolt/adapter/__init__.py +++ b/slack_bolt/adapter/__init__.py @@ -1,2 +1 @@ -"""Adapter modules for running Bolt apps along with Web frameworks or Socket Mode. -""" +"""Adapter modules for running Bolt apps along with Web frameworks or Socket Mode.""" diff --git a/slack_bolt/adapter/socket_mode/async_internals.py b/slack_bolt/adapter/socket_mode/async_internals.py index c2965f766..55683d8bf 100644 --- a/slack_bolt/adapter/socket_mode/async_internals.py +++ b/slack_bolt/adapter/socket_mode/async_internals.py @@ -3,6 +3,7 @@ import json import logging from time import time +from typing import Dict, Union, Sequence from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest @@ -14,7 +15,13 @@ async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest): - bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload) + 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 + + bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload, headers=headers) 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..7e128f87d 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, Union, Sequence from slack_sdk.socket_mode.client import BaseSocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest @@ -14,7 +15,13 @@ def run_bolt_app(app: App, req: SocketModeRequest): - bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload) + 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 + + bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload, headers=headers) bolt_resp: BoltResponse = app.dispatch(bolt_req) return bolt_resp diff --git a/slack_bolt/context/base_context.py b/slack_bolt/context/base_context.py index 843d5ef60..012df4e8c 100644 --- a/slack_bolt/context/base_context.py +++ b/slack_bolt/context/base_context.py @@ -38,6 +38,8 @@ class BaseContext(dict): "set_status", "set_title", "set_suggested_prompts", + "retry_num", + "retry_reason", ] # Note that these items are not copyable, so when you add new items to this list, # you must modify ThreadListenerRunner/AsyncioListenerRunner's _build_lazy_request method to pass the values. @@ -173,6 +175,16 @@ def user_token(self) -> Optional[str]: """The user token resolved for this request.""" return self.get("user_token") + @property + def retry_num(self) -> Optional[int]: + """The retry number for this request (X-Slack-Retry-Num header in HTTP mode, retry_attempt in Socket Mode).""" + return self.get("retry_num") + + @property + def retry_reason(self) -> Optional[str]: + """The retry reason for this request (X-Slack-Retry-Reason header in HTTP mode, retry_reason in Socket Mode).""" + return self.get("retry_reason") + def set_authorize_result(self, authorize_result: AuthorizeResult): self["authorize_result"] = authorize_result if authorize_result.bot_id is not None: diff --git a/slack_bolt/request/async_internals.py b/slack_bolt/request/async_internals.py index ea94739e8..3d155eeb5 100644 --- a/slack_bolt/request/async_internals.py +++ b/slack_bolt/request/async_internals.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Dict, Any, Optional, Sequence from slack_bolt.context.async_context import AsyncBoltContext from slack_bolt.request.internals import ( @@ -21,6 +21,7 @@ def build_async_context( context: AsyncBoltContext, body: Dict[str, Any], + headers: Optional[Dict[str, Sequence[str]]] = None, ) -> AsyncBoltContext: context["is_enterprise_install"] = extract_is_enterprise_install(body) enterprise_id = extract_enterprise_id(body) @@ -67,4 +68,17 @@ def build_async_context( context.logger.debug(debug_multiple_response_urls_detected()) response_url = response_urls[0].get("response_url") context["response_url"] = response_url + + if headers is not None: + retry_num_header = headers.get("x-slack-retry-num") + if retry_num_header is not None and len(retry_num_header) > 0: + try: + context["retry_num"] = int(retry_num_header[0]) + except (ValueError, TypeError): + pass + + retry_reason_header = headers.get("x-slack-retry-reason") + if retry_reason_header is not None and len(retry_reason_header) > 0: + context["retry_reason"] = retry_reason_header[0] + return context diff --git a/slack_bolt/request/async_request.py b/slack_bolt/request/async_request.py index 73891446e..fd6e86bbc 100644 --- a/slack_bolt/request/async_request.py +++ b/slack_bolt/request/async_request.py @@ -67,7 +67,7 @@ def __init__( else: self.body = {} - self.context = build_async_context(AsyncBoltContext(context if context else {}), self.body) + self.context = build_async_context(AsyncBoltContext(context if context else {}), self.body, self.headers) self.lazy_only = bool(self.headers.get("x-slack-bolt-lazy-only", [False])[0]) self.lazy_function_name = self.headers.get("x-slack-bolt-lazy-function-name", [None])[0] self.mode = mode diff --git a/slack_bolt/request/internals.py b/slack_bolt/request/internals.py index 014a8134a..fac4449f5 100644 --- a/slack_bolt/request/internals.py +++ b/slack_bolt/request/internals.py @@ -268,7 +268,9 @@ def extract_function_inputs(payload: Dict[str, Any]) -> Optional[Dict[str, Any]] return None -def build_context(context: BoltContext, body: Dict[str, Any]) -> BoltContext: +def build_context( + context: BoltContext, body: Dict[str, Any], headers: Optional[Dict[str, Sequence[str]]] = None +) -> BoltContext: context["is_enterprise_install"] = extract_is_enterprise_install(body) enterprise_id = extract_enterprise_id(body) if enterprise_id: @@ -314,6 +316,19 @@ def build_context(context: BoltContext, body: Dict[str, Any]) -> BoltContext: context.logger.debug(debug_multiple_response_urls_detected()) response_url = response_urls[0].get("response_url") context["response_url"] = response_url + + if headers is not None: + retry_num_header = headers.get("x-slack-retry-num") + if retry_num_header is not None and len(retry_num_header) > 0: + try: + context["retry_num"] = int(retry_num_header[0]) + except (ValueError, TypeError): + pass + + retry_reason_header = headers.get("x-slack-retry-reason") + if retry_reason_header is not None and len(retry_reason_header) > 0: + context["retry_reason"] = retry_reason_header[0] + return context diff --git a/slack_bolt/request/request.py b/slack_bolt/request/request.py index 2a418a33f..1d0718c98 100644 --- a/slack_bolt/request/request.py +++ b/slack_bolt/request/request.py @@ -66,7 +66,7 @@ def __init__( else: self.body = {} - self.context = build_context(BoltContext(context if context else {}), self.body) + self.context = build_context(BoltContext(context if context else {}), self.body, self.headers) self.lazy_only = bool(self.headers.get("x-slack-bolt-lazy-only", [False])[0]) self.lazy_function_name = self.headers.get("x-slack-bolt-lazy-function-name", [None])[0] self.mode = mode diff --git a/tests/scenario_tests/test_events.py b/tests/scenario_tests/test_events.py index 0cc6641fa..7703d6b23 100644 --- a/tests/scenario_tests/test_events.py +++ b/tests/scenario_tests/test_events.py @@ -580,6 +580,28 @@ def handle_app_mention(body, say, payload, event): assert_auth_test_count(self, 1) assert_received_request_count(self, path="/chat.postMessage", min_count=1) + def test_retry_headers_http_mode(self): + app = App(client=self.web_client, signing_secret=self.signing_secret) + + retry_num_received = [] + retry_reason_received = [] + + @app.event("app_mention") + def handle_app_mention(context: BoltContext): + retry_num_received.append(context.retry_num) + retry_reason_received.append(context.retry_reason) + + timestamp, body = str(int(time())), json.dumps(self.valid_event_body) + headers = self.build_headers(timestamp, body) + headers["x-slack-retry-num"] = ["2"] + headers["x-slack-retry-reason"] = ["http_error"] + + request: BoltRequest = BoltRequest(body=body, headers=headers) + response = app.dispatch(request) + assert response.status == 200 + assert retry_num_received[0] == 2 + assert retry_reason_received[0] == "http_error" + def my_decorator(f): @wraps(f) diff --git a/tests/scenario_tests/test_events_socket_mode.py b/tests/scenario_tests/test_events_socket_mode.py index 81c45b148..8f74992ba 100644 --- a/tests/scenario_tests/test_events_socket_mode.py +++ b/tests/scenario_tests/test_events_socket_mode.py @@ -342,3 +342,27 @@ def handler2(say: Say): assert_auth_test_count(self, 1) assert_received_request_count(self, path="/chat.postMessage", min_count=2) + + def test_retry_headers_socket_mode(self): + from slack_bolt import BoltContext + + app = App(client=self.web_client) + + retry_num_received = [] + retry_reason_received = [] + + @app.event("app_mention") + def handle_app_mention(context: BoltContext): + retry_num_received.append(context.retry_num) + retry_reason_received.append(context.retry_reason) + + headers = { + "X-Slack-Retry-Num": ["3"], + "X-Slack-Retry-Reason": ["timeout"], + } + + request: BoltRequest = BoltRequest(body=self.valid_event_body, mode="socket_mode", headers=headers) + response = app.dispatch(request) + assert response.status == 200 + assert retry_num_received[0] == 3 + assert retry_reason_received[0] == "timeout"