From 9544dd696ac9f16b5ebd6231a71e38f06b04cea1 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:37:54 +0000 Subject: [PATCH] fix(slack-bot): return 400 on malformed request bodies (#254) Bolt parse_body raises JSONDecodeError for empty or non-JSON form payload fields, which turned probe POSTs into unhandled Lambda 500s. Fixes AFTERHOURS-SHIFT-MANAGER-2 --- src/slack-bot/handler.py | 7 ++- tests/slack_bot/conftest.py | 29 +++++++++++++ tests/slack_bot/test_handler.py | 76 +++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/slack_bot/test_handler.py diff --git a/src/slack-bot/handler.py b/src/slack-bot/handler.py index 11c899f..ba0a2ca 100644 --- a/src/slack-bot/handler.py +++ b/src/slack-bot/handler.py @@ -1,5 +1,6 @@ """Lambda handler — Slack Bolt app entry point.""" +import json import logging import os @@ -33,4 +34,8 @@ def _get_handler() -> SlackRequestHandler: def handler(event, context): - return _get_handler().handle(event, context) + try: + return _get_handler().handle(event, context) + except json.JSONDecodeError: + logger.warning("rejecting malformed slack request body") + return {"statusCode": 400, "body": "invalid request"} diff --git a/tests/slack_bot/conftest.py b/tests/slack_bot/conftest.py index 68ef287..74bedd1 100644 --- a/tests/slack_bot/conftest.py +++ b/tests/slack_bot/conftest.py @@ -24,6 +24,35 @@ def slackbot_app(): return _load("slackbot_app", "src/slack-bot/app.py") +@pytest.fixture +def slackbot_handler(monkeypatch): + """Load handler.py; skip Slack auth.test so request parsing is the unit.""" + from slack_bolt import App + from slack_bolt.adapter.aws_lambda import SlackRequestHandler + + slack_bot_dir = str(_ROOT / "src/slack-bot") + monkeypatch.syspath_prepend(slack_bot_dir) + saved = {name: sys.modules.get(name) for name in ("app", "handler")} + for name in saved: + sys.modules.pop(name, None) + mod = _load("slackbot_handler", "src/slack-bot/handler.py") + bolt_app = App( + token="xoxb-test", + signing_secret="test-secret", + process_before_response=True, + token_verification_enabled=False, + ) + wrapped = SlackRequestHandler(app=bolt_app) + monkeypatch.setattr(mod, "_get_handler", lambda: wrapped) + yield mod + mod._slack_handler = None + for name, previous in saved.items(): + if previous is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous + + @pytest.fixture def respond(): return MagicMock(name="respond") diff --git a/tests/slack_bot/test_handler.py b/tests/slack_bot/test_handler.py new file mode 100644 index 0000000..b397709 --- /dev/null +++ b/tests/slack_bot/test_handler.py @@ -0,0 +1,76 @@ +"""Slack-bot Lambda handler: malformed bodies return 400, not 500.""" + +import json +from types import SimpleNamespace + +import pytest +from slack_bolt.request.internals import parse_body + + +def _context(): + return SimpleNamespace( + function_name="afterhours-shift-manager", + invoked_function_arn="arn:aws:lambda:us-east-1:1:function:x", + ) + + +def _event(body, content_type, user_agent="Python-urllib/3.12"): + return { + "version": "2.0", + "routeKey": "POST /slack/events", + "rawPath": "/slack/events", + "headers": { + "content-type": content_type, + "user-agent": user_agent, + }, + "requestContext": {"http": {"method": "POST", "path": "/slack/events"}}, + "body": body, + "isBase64Encoded": False, + "queryStringParameters": {}, + } + + +def test_parse_body_empty_payload_raises(): + with pytest.raises(json.JSONDecodeError): + parse_body("payload=", "application/x-www-form-urlencoded") + + +def test_parse_body_non_json_payload_raises(): + with pytest.raises(json.JSONDecodeError): + parse_body("payload=not-json", "application/x-www-form-urlencoded") + + +def test_empty_form_payload_returns_400(slackbot_handler): + result = slackbot_handler.handler( + _event("payload=", "application/x-www-form-urlencoded"), _context() + ) + assert result["statusCode"] == 400 + assert result["body"] == "invalid request" + + +def test_non_json_form_payload_returns_400(slackbot_handler): + result = slackbot_handler.handler( + _event("payload=not-json", "application/x-www-form-urlencoded"), + _context(), + ) + assert result["statusCode"] == 400 + assert result["body"] == "invalid request" + + +def test_payload_substring_without_form_key_is_unsigned(slackbot_handler): + result = slackbot_handler.handler( + _event("hello payload world", "text/plain"), _context() + ) + assert result["statusCode"] in (401, 403) + + +def test_json_events_body_is_unsigned_not_jsondecode(slackbot_handler): + result = slackbot_handler.handler( + _event( + '{"type":"url_verification","challenge":"abc"}', + "application/json", + user_agent="Slackbot 1.0", + ), + _context(), + ) + assert result["statusCode"] in (401, 403)