diff --git a/slack_bolt/adapter/socket_mode/async_internals.py b/slack_bolt/adapter/socket_mode/async_internals.py index 5fefd7400..438dc1e16 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 from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest @@ -20,6 +21,19 @@ async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest): return bolt_resp +def _to_payload(content_type: str, body: str) -> Dict: + # A text-only ack is labeled as JSON when it starts with "{". + # Only treat it as JSON when it really parses to an object; otherwise send it as text. + if content_type.startswith("application/json"): + try: + parsed = json.loads(body) + if isinstance(parsed, dict): + return parsed + except ValueError: + pass + return {"text": body} + + async def send_async_response( client: AsyncBaseSocketModeClient, req: SocketModeRequest, @@ -30,15 +44,9 @@ async def send_async_response( content_type = bolt_resp.headers.get("content-type", [""])[0] if bolt_resp.body is None or len(bolt_resp.body) == 0: await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id)) - elif content_type.startswith("application/json"): - dict_body = json.loads(bolt_resp.body) - await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body)) else: await client.send_socket_mode_response( - SocketModeResponse( - envelope_id=req.envelope_id, - payload={"text": bolt_resp.body}, - ) + SocketModeResponse(envelope_id=req.envelope_id, payload=_to_payload(content_type, bolt_resp.body)) ) if client.logger.level <= logging.DEBUG: spent_time = int((time() - start_time) * 1000) diff --git a/slack_bolt/adapter/socket_mode/internals.py b/slack_bolt/adapter/socket_mode/internals.py index b57a1c3d7..8aa924d51 100644 --- a/slack_bolt/adapter/socket_mode/internals.py +++ b/slack_bolt/adapter/socket_mode/internals.py @@ -30,6 +30,19 @@ def run_bolt_app(app: App, req: SocketModeRequest): return bolt_resp +def _to_payload(content_type: str, body: str) -> Dict: + # A text-only ack is labeled as JSON when it starts with "{". + # Only treat it as JSON when it really parses to an object; otherwise send it as text. + if content_type.startswith("application/json"): + try: + parsed = json.loads(body) + if isinstance(parsed, dict): + return parsed + except ValueError: + pass + return {"text": body} + + def send_response( client: BaseSocketModeClient, req: SocketModeRequest, @@ -40,12 +53,9 @@ def send_response( content_type = bolt_resp.headers.get("content-type", [""])[0] if bolt_resp.body is None or len(bolt_resp.body) == 0: client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id)) - elif content_type.startswith("application/json"): - dict_body = json.loads(bolt_resp.body) - client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body)) else: client.send_socket_mode_response( - SocketModeResponse(envelope_id=req.envelope_id, payload={"text": bolt_resp.body}) + SocketModeResponse(envelope_id=req.envelope_id, payload=_to_payload(content_type, bolt_resp.body)) ) if client.logger.level <= logging.DEBUG: diff --git a/tests/adapter_tests/socket_mode/test_internals.py b/tests/adapter_tests/socket_mode/test_internals.py index fede30b48..5ef366811 100644 --- a/tests/adapter_tests/socket_mode/test_internals.py +++ b/tests/adapter_tests/socket_mode/test_internals.py @@ -18,3 +18,26 @@ def test_build_retry_headers_with_retry(self): ) headers = build_headers(req) assert headers == {"x-slack-retry-num": "2", "x-slack-retry-reason": "http_timeout"} + + def test_send_response_text_starting_with_brace(self): + # A text ack that happens to start with "{" is not JSON and must be sent as text + from slack_bolt.adapter.socket_mode.internals import send_response + from slack_bolt.response import BoltResponse + import logging + + sent = [] + + class FakeClient: + logger = logging.getLogger("test") + + def send_socket_mode_response(self, response): + sent.append(response) + + req = SocketModeRequest(type="slash_commands", envelope_id="e1", payload={"command": "/echo"}) + send_response(FakeClient(), req, BoltResponse(status=200, body="{foo"), 0.0) + assert len(sent) == 1 + assert sent[0].payload == {"text": "{foo"} + + sent.clear() + send_response(FakeClient(), req, BoltResponse(status=200, body='{"text": "hi"}'), 0.0) + assert sent[0].payload == {"text": "hi"} diff --git a/tests/adapter_tests_async/socket_mode/test_async_internals.py b/tests/adapter_tests_async/socket_mode/test_async_internals.py new file mode 100644 index 000000000..27a6c193a --- /dev/null +++ b/tests/adapter_tests_async/socket_mode/test_async_internals.py @@ -0,0 +1,29 @@ +import logging + +import pytest +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_bolt.adapter.socket_mode.async_internals import send_async_response +from slack_bolt.response import BoltResponse + + +class TestAsyncSocketModeInternals: + @pytest.mark.asyncio + async def test_send_response_text_starting_with_brace(self): + # A text ack that happens to start with "{" is not JSON and must be sent as text + sent = [] + + class FakeClient: + logger = logging.getLogger("test") + + async def send_socket_mode_response(self, response): + sent.append(response) + + req = SocketModeRequest(type="slash_commands", envelope_id="e1", payload={"command": "/echo"}) + await send_async_response(FakeClient(), req, BoltResponse(status=200, body="{foo"), 0.0) + assert len(sent) == 1 + assert sent[0].payload == {"text": "{foo"} + + sent.clear() + await send_async_response(FakeClient(), req, BoltResponse(status=200, body='{"text": "hi"}'), 0.0) + assert sent[0].payload == {"text": "hi"}