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
This commit is contained in:
Adam Moussa 2026-09-16 16:37:54 +00:00 • committed by GitHub
parent 7a513b30ca
commit 9544dd696a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 1 deletions

View file

@ -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):
try:
return _get_handler().handle(event, context)
except json.JSONDecodeError:
logger.warning("rejecting malformed slack request body")
return {"statusCode": 400, "body": "invalid request"}

View file

@ -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")

View file

@ -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)