Bound Slack response_url error reads

This commit is contained in:
luyifan 2026-06-30 10:14:43 +08:00 committed by Teknium
parent f36c748c85
commit f8aef2e4e0
2 changed files with 99 additions and 1 deletions

View file

@ -64,6 +64,36 @@ except ImportError: # pragma: no cover - plugin loaded outside package context
logger = logging.getLogger(__name__)
_SLACK_ERROR_BODY_LIMIT_BYTES = 8 * 1024
async def _read_error_text_limited(
response: Any,
*,
limit: int = _SLACK_ERROR_BODY_LIMIT_BYTES,
) -> str:
content = getattr(response, "content", None)
read = getattr(content, "read", None)
if callable(read):
chunks: list[bytes] = []
total = 0
while total <= limit:
size = min(4096, limit + 1 - total)
chunk = await read(size)
if not chunk:
break
data = bytes(chunk)
chunks.append(data)
total += len(data)
if total > limit:
release = getattr(response, "release", None)
if callable(release):
release()
return b"".join(chunks)[:limit].decode("utf-8", errors="replace")
text = await response.text()
return str(text)[:limit]
# ContextVar carrying the user_id of the slash-command invoker.
# Set in _handle_slash_command, read in send() to match the correct
# stashed response_url when multiple users issue commands on the same
@ -1175,7 +1205,7 @@ class SlackAdapter(BasePlatformAdapter):
) as resp:
if resp.status == 200:
return SendResult(success=True, message_id=None)
body = await resp.text()
body = await _read_error_text_limited(resp)
logger.warning(
"[Slack] response_url POST returned %s: %s",
resp.status,

View file

@ -5322,6 +5322,7 @@ class TestSlashEphemeralAck:
mock_resp = AsyncMock()
mock_resp.status = 500
mock_resp.content = None
mock_resp.text = AsyncMock(return_value="Internal Server Error")
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
mock_resp.__aexit__ = AsyncMock(return_value=False)
@ -5343,6 +5344,73 @@ class TestSlashEphemeralAck:
# Still success — the user saw the initial ack already
assert result.success is True
@pytest.mark.asyncio
async def test_send_slash_ephemeral_limits_error_body(self, adapter):
"""response_url failures should not read oversized bodies unbounded."""
import time
class _FakeContent:
def __init__(self, payload: bytes):
self._payload = payload
self._offset = 0
self.bytes_read = 0
async def read(self, size: int = -1):
if size is None or size < 0:
size = len(self._payload) - self._offset
chunk = self._payload[self._offset : self._offset + size]
self._offset += len(chunk)
self.bytes_read += len(chunk)
return chunk
class _FakeResponse:
status = 500
def __init__(self, text: str):
self.content = _FakeContent(text.encode("utf-8"))
self.text_calls = 0
self.released = False
async def text(self):
self.text_calls += 1
return "should not be called"
def release(self):
self.released = True
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
adapter._slash_command_contexts[("C1", "U1")] = {
"response_url": "https://hooks.slack.com/commands/oversized",
"ts": time.monotonic(),
}
response = _FakeResponse(
("slack response_url failure " * 1000) + "tail-marker"
)
mock_session = AsyncMock()
mock_session.post = MagicMock(return_value=response)
mock_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_session.__aexit__ = AsyncMock(return_value=False)
with patch(
"plugins.platforms.slack.adapter.aiohttp.ClientSession",
return_value=mock_session,
):
result = await adapter.send("C1", "Some response")
assert result.success is True
assert response.text_calls == 0
assert (
response.content.bytes_read
== _slack_mod._SLACK_ERROR_BODY_LIMIT_BYTES + 1
)
assert response.released is True
@pytest.mark.asyncio
async def test_send_slash_ephemeral_fallback_on_exception(self, adapter):
"""_send_slash_ephemeral returns success=True even if aiohttp raises."""