Skip to content

Commit 497eef3

Browse files
committed
fix(socket_mode): text acks starting with '{' were never sent
BoltResponse labels any body that starts with '{' as JSON. A text ack such as ack('{foo') carries that label, so the Socket Mode adapters called json.loads on it, raised JSONDecodeError, and never sent the envelope response. Slack then showed a timeout to the user. - Only send a JSON payload when the body really parses to an object; otherwise send it as text - Apply to both the sync and async adapters - Add tests for a text ack that starts with '{' and a real JSON ack
1 parent 698687c commit 497eef3

4 files changed

Lines changed: 81 additions & 11 deletions

File tree

‎slack_bolt/adapter/socket_mode/async_internals.py‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
from time import time
6+
from typing import Dict
67

78
from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient
89
from slack_sdk.socket_mode.request import SocketModeRequest
@@ -20,6 +21,19 @@ async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest):
2021
return bolt_resp
2122

2223

24+
def _to_payload(content_type: str, body: str) -> Dict:
25+
# A text-only ack is labeled as JSON when it starts with "{".
26+
# Only treat it as JSON when it really parses to an object; otherwise send it as text.
27+
if content_type.startswith("application/json"):
28+
try:
29+
parsed = json.loads(body)
30+
if isinstance(parsed, dict):
31+
return parsed
32+
except ValueError:
33+
pass
34+
return {"text": body}
35+
36+
2337
async def send_async_response(
2438
client: AsyncBaseSocketModeClient,
2539
req: SocketModeRequest,
@@ -30,15 +44,9 @@ async def send_async_response(
3044
content_type = bolt_resp.headers.get("content-type", [""])[0]
3145
if bolt_resp.body is None or len(bolt_resp.body) == 0:
3246
await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id))
33-
elif content_type.startswith("application/json"):
34-
dict_body = json.loads(bolt_resp.body)
35-
await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body))
3647
else:
3748
await client.send_socket_mode_response(
38-
SocketModeResponse(
39-
envelope_id=req.envelope_id,
40-
payload={"text": bolt_resp.body},
41-
)
49+
SocketModeResponse(envelope_id=req.envelope_id, payload=_to_payload(content_type, bolt_resp.body))
4250
)
4351
if client.logger.level <= logging.DEBUG:
4452
spent_time = int((time() - start_time) * 1000)

‎slack_bolt/adapter/socket_mode/internals.py‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ def run_bolt_app(app: App, req: SocketModeRequest):
3030
return bolt_resp
3131

3232

33+
def _to_payload(content_type: str, body: str) -> Dict:
34+
# A text-only ack is labeled as JSON when it starts with "{".
35+
# Only treat it as JSON when it really parses to an object; otherwise send it as text.
36+
if content_type.startswith("application/json"):
37+
try:
38+
parsed = json.loads(body)
39+
if isinstance(parsed, dict):
40+
return parsed
41+
except ValueError:
42+
pass
43+
return {"text": body}
44+
45+
3346
def send_response(
3447
client: BaseSocketModeClient,
3548
req: SocketModeRequest,
@@ -40,12 +53,9 @@ def send_response(
4053
content_type = bolt_resp.headers.get("content-type", [""])[0]
4154
if bolt_resp.body is None or len(bolt_resp.body) == 0:
4255
client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id))
43-
elif content_type.startswith("application/json"):
44-
dict_body = json.loads(bolt_resp.body)
45-
client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body))
4656
else:
4757
client.send_socket_mode_response(
48-
SocketModeResponse(envelope_id=req.envelope_id, payload={"text": bolt_resp.body})
58+
SocketModeResponse(envelope_id=req.envelope_id, payload=_to_payload(content_type, bolt_resp.body))
4959
)
5060

5161
if client.logger.level <= logging.DEBUG:

‎tests/adapter_tests/socket_mode/test_internals.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,26 @@ def test_build_retry_headers_with_retry(self):
1818
)
1919
headers = build_headers(req)
2020
assert headers == {"x-slack-retry-num": "2", "x-slack-retry-reason": "http_timeout"}
21+
22+
def test_send_response_text_starting_with_brace(self):
23+
# A text ack that happens to start with "{" is not JSON and must be sent as text
24+
from slack_bolt.adapter.socket_mode.internals import send_response
25+
from slack_bolt.response import BoltResponse
26+
import logging
27+
28+
sent = []
29+
30+
class FakeClient:
31+
logger = logging.getLogger("test")
32+
33+
def send_socket_mode_response(self, response):
34+
sent.append(response)
35+
36+
req = SocketModeRequest(type="slash_commands", envelope_id="e1", payload={"command": "/echo"})
37+
send_response(FakeClient(), req, BoltResponse(status=200, body="{foo"), 0.0)
38+
assert len(sent) == 1
39+
assert sent[0].payload == {"text": "{foo"}
40+
41+
sent.clear()
42+
send_response(FakeClient(), req, BoltResponse(status=200, body='{"text": "hi"}'), 0.0)
43+
assert sent[0].payload == {"text": "hi"}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import logging
2+
3+
import pytest
4+
from slack_sdk.socket_mode.request import SocketModeRequest
5+
6+
from slack_bolt.adapter.socket_mode.async_internals import send_async_response
7+
from slack_bolt.response import BoltResponse
8+
9+
10+
class TestAsyncSocketModeInternals:
11+
@pytest.mark.asyncio
12+
async def test_send_response_text_starting_with_brace(self):
13+
# A text ack that happens to start with "{" is not JSON and must be sent as text
14+
sent = []
15+
16+
class FakeClient:
17+
logger = logging.getLogger("test")
18+
19+
async def send_socket_mode_response(self, response):
20+
sent.append(response)
21+
22+
req = SocketModeRequest(type="slash_commands", envelope_id="e1", payload={"command": "/echo"})
23+
await send_async_response(FakeClient(), req, BoltResponse(status=200, body="{foo"), 0.0)
24+
assert len(sent) == 1
25+
assert sent[0].payload == {"text": "{foo"}
26+
27+
sent.clear()
28+
await send_async_response(FakeClient(), req, BoltResponse(status=200, body='{"text": "hi"}'), 0.0)
29+
assert sent[0].payload == {"text": "hi"}

0 commit comments

Comments
 (0)