Skip to content

Socket Mode: retry_attempt and retry_reason are dropped when building AsyncBoltRequest #1484

Description

@loganrosen

Summary

In Socket Mode, SocketModeRequest.retry_attempt and SocketModeRequest.retry_reason are silently dropped when constructing the AsyncBoltRequest. This makes it impossible for middleware/listeners running in Socket Mode to detect Slack Events API retries — even though the same information is readily available to HTTP-mode handlers via X-Slack-Retry-Num / X-Slack-Retry-Reason headers.

Reproduction

fromslack_bolt.request.async_requestimportAsyncBoltRequestfromslack_sdk.socket_mode.requestimportSocketModeRequestenvelope= {
"type": "events_api",
"envelope_id": "abc-123",
"payload": {
"type": "event_callback",
"event_id": "Ev123",
"event": {"type": "app_mention", "text": "<@U1> hello"},
},
"accepts_response_payload": False,
"retry_attempt": 2,
"retry_reason": "http_timeout",
}
sm_req=SocketModeRequest.from_dict(envelope)
print(sm_req.retry_attempt) # 2print(sm_req.retry_reason) # "http_timeout"# What slack_bolt does in run_async_bolt_app:bolt_req=AsyncBoltRequest(mode="socket_mode", body=sm_req.payload)
print(bolt_req.headers) # {}print("retry_attempt"inbolt_req.body) # Falseprint(list(bolt_req.body.keys())) # ['type', 'event_id', 'event']

Root Cause

slack_bolt/adapter/socket_mode/async_internals.py:

asyncdefrun_async_bolt_app(app: AsyncApp, req: SocketModeRequest):
bolt_req: AsyncBoltRequest=AsyncBoltRequest(mode="socket_mode", body=req.payload)
...

Only req.payload is propagated. The retry fields on the SocketModeRequest envelope (which sit alongside payload, not inside it) are not carried into the bolt request. The sync version in internals.py has the same issue.

Why this matters

Slack's Events API automatically retries delivery if the receiver doesn't ack within 3s. Apps need to detect retries to:

  • Idempotently handle events (avoid double-processing)
  • Distinguish "silently dropped envelopes" from "retried envelopes" in observability metrics
  • Log retry context (e.g., retry_reason="http_timeout") for debugging

In HTTP mode this works via headers (request.headers.get("x-slack-retry-num") is the documented pattern). In Socket Mode there is currently no documented or convenient way to access this from middleware.

Issue #868 included a maintainer comment suggesting retry_attempt exists "as a top-level property in body" for socket mode, but as shown above, it does not — only req.payload (which excludes retry_attempt) becomes bolt_req.body.

Proposed Fix

Propagate retry info into the bolt request. Two reasonable approaches:

Option 1 — synthesize headers (mirrors HTTP mode; existing retry-detection patterns work unchanged):

asyncdefrun_async_bolt_app(app: AsyncApp, req: SocketModeRequest):
headers= {}
ifreq.retry_attemptisnotNone:
headers["x-slack-retry-num"] =str(req.retry_attempt)
ifreq.retry_reasonisnotNone:
headers["x-slack-retry-reason"] =req.retry_reasonbolt_req=AsyncBoltRequest(mode="socket_mode", body=req.payload, headers=headersorNone)
...

Option 2 — surface via context:

context= {"retry_attempt": req.retry_attempt, "retry_reason": req.retry_reason}
bolt_req=AsyncBoltRequest(mode="socket_mode", body=req.payload, context=context)

Option 1 has the advantage that existing HTTP-mode retry-detection code works unchanged in Socket Mode.

Happy to send a PR if there's interest.

Versions

  • slack_bolt==1.28.0 (latest as of writing — same code on main)
  • slack_sdk==3.40.0
  • Python 3.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions