From f62cda5d7ee6c78fc448f91f34a3be3662756cdf Mon Sep 17 00:00:00 2001 From: Sahil Jagtap Date: Wed, 7 Oct 2026 20:09:48 -0400 Subject: [PATCH 1/2] fix(aws_lambda): handle null headers, query string, and body in API Gateway events API Gateway sends null, not a missing key, for request fields that have no value. For example "headers", "multiValueHeaders" and "queryStringParameters" are null when the request has none of them. The Lambda adapter assumed these were always dicts and raised AttributeError before the request was dispatched. It also raised KeyError when "isBase64Encoded" was absent, and TypeError when the flag was true but the body was null. - Treat None the same as a missing key for every event field - Copy the headers dict so the caller's event is not modified - Add tests for null fields, a missing base64 flag, and a real base64-encoded body --- slack_bolt/adapter/aws_lambda/handler.py | 22 +++++---- tests/adapter_tests/aws/test_aws_lambda.py | 56 ++++++++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/slack_bolt/adapter/aws_lambda/handler.py b/slack_bolt/adapter/aws_lambda/handler.py index 4d155e58f..8b1bc1356 100644 --- a/slack_bolt/adapter/aws_lambda/handler.py +++ b/slack_bolt/adapter/aws_lambda/handler.py @@ -71,22 +71,26 @@ def handle(self, event, context): def to_bolt_request(event) -> BoltRequest: - body = event.get("body", "") - if event["isBase64Encoded"]: + # API Gateway sends null (not a missing key) for fields that have no value, + # such as "headers", "multiValueHeaders" and "queryStringParameters". + # Every lookup below treats None the same as a missing key. + body = event.get("body") or "" + if event.get("isBase64Encoded") is True and body: body = base64.b64decode(body).decode("utf-8") - cookies: Sequence[str] = event.get("cookies", []) - if cookies is None or len(cookies) == 0: + cookies: Sequence[str] = event.get("cookies") or [] + if len(cookies) == 0: # In the case of format v1 - multiValueHeaders = event.get("multiValueHeaders", {}) - cookies = multiValueHeaders.get("cookie", []) + multiValueHeaders = event.get("multiValueHeaders") or {} + cookies = multiValueHeaders.get("cookie") or [] if len(cookies) == 0: # Try using uppercase - cookies = multiValueHeaders.get("Cookie", []) - headers = event.get("headers", {}) + cookies = multiValueHeaders.get("Cookie") or [] + # Copy the headers so the caller's event dict is left untouched + headers = dict(event.get("headers") or {}) headers["cookie"] = cookies return BoltRequest( body=body, - query=event.get("queryStringParameters", {}), + query=event.get("queryStringParameters") or {}, headers=headers, ) diff --git a/tests/adapter_tests/aws/test_aws_lambda.py b/tests/adapter_tests/aws/test_aws_lambda.py index c2e888fb7..52414f15b 100644 --- a/tests/adapter_tests/aws/test_aws_lambda.py +++ b/tests/adapter_tests/aws/test_aws_lambda.py @@ -1,3 +1,4 @@ +import base64 import json from time import time from urllib.parse import quote @@ -70,6 +71,61 @@ def test_not_found(self): response = not_found() assert response["statusCode"] == 404 + def test_null_fields_in_api_gateway_event(self): + # API Gateway sends null for headers, multiValueHeaders and queryStringParameters + # when the request has none. These used to crash before the request was dispatched. + app = App(client=self.web_client, signing_secret=self.signing_secret) + event = { + "httpMethod": "POST", + "requestContext": {"httpMethod": "POST"}, + "headers": None, + "multiValueHeaders": None, + "queryStringParameters": None, + "body": "{}", + "isBase64Encoded": False, + } + response = SlackRequestHandler(app).handle(event, self.context) + # No signature headers, so the request is rejected rather than crashing + assert response["statusCode"] == 401 + # The caller's event must not be modified + assert event["headers"] is None + + def test_missing_is_base64_encoded_and_null_body(self): + app = App(client=self.web_client, signing_secret=self.signing_secret) + # isBase64Encoded is absent (for example when invoked from a test tool) + event = {"requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": "{}"} + assert SlackRequestHandler(app).handle(event, self.context)["statusCode"] == 401 + # isBase64Encoded is true but the body is null + event = {"requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": None, "isBase64Encoded": True} + assert SlackRequestHandler(app).handle(event, self.context)["statusCode"] == 401 + + def test_base64_encoded_body(self): + app = App(client=self.web_client, signing_secret=self.signing_secret) + + def event_handler(): + pass + + app.event("app_mention")(event_handler) + input = { + "token": "verification_token", + "team_id": "T111", + "api_app_id": "A111", + "event": {"type": "app_mention", "text": "<@W111> Hi", "user": "W222", "team": "T111", "channel": "C111"}, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, + } + timestamp, body = str(int(time())), json.dumps(input) + event = { + "body": base64.b64encode(body.encode("utf-8")).decode("ascii"), + "queryStringParameters": None, + "headers": self.build_headers(timestamp, body), + "requestContext": {"http": {"method": "POST"}}, + "isBase64Encoded": True, + } + response = SlackRequestHandler(app).handle(event, self.context) + assert response["statusCode"] == 200 + def test_first_value(self): assert _first_value({"foo": [1, 2, 3]}, "foo") == 1 assert _first_value({"foo": []}, "foo") is None From cd85e58cd1849d758010a11ada2a73d649468b49 Mon Sep 17 00:00:00 2001 From: Sahil Jagtap Date: Thu, 8 Oct 2026 17:02:32 -0400 Subject: [PATCH 2/2] chore(aws_lambda): remove inline comments per review --- slack_bolt/adapter/aws_lambda/handler.py | 4 ---- tests/adapter_tests/aws/test_aws_lambda.py | 6 ------ 2 files changed, 10 deletions(-) diff --git a/slack_bolt/adapter/aws_lambda/handler.py b/slack_bolt/adapter/aws_lambda/handler.py index 8b1bc1356..270d48831 100644 --- a/slack_bolt/adapter/aws_lambda/handler.py +++ b/slack_bolt/adapter/aws_lambda/handler.py @@ -71,9 +71,6 @@ def handle(self, event, context): def to_bolt_request(event) -> BoltRequest: - # API Gateway sends null (not a missing key) for fields that have no value, - # such as "headers", "multiValueHeaders" and "queryStringParameters". - # Every lookup below treats None the same as a missing key. body = event.get("body") or "" if event.get("isBase64Encoded") is True and body: body = base64.b64decode(body).decode("utf-8") @@ -85,7 +82,6 @@ def to_bolt_request(event) -> BoltRequest: if len(cookies) == 0: # Try using uppercase cookies = multiValueHeaders.get("Cookie") or [] - # Copy the headers so the caller's event dict is left untouched headers = dict(event.get("headers") or {}) headers["cookie"] = cookies return BoltRequest( diff --git a/tests/adapter_tests/aws/test_aws_lambda.py b/tests/adapter_tests/aws/test_aws_lambda.py index 52414f15b..3827af9b8 100644 --- a/tests/adapter_tests/aws/test_aws_lambda.py +++ b/tests/adapter_tests/aws/test_aws_lambda.py @@ -72,8 +72,6 @@ def test_not_found(self): assert response["statusCode"] == 404 def test_null_fields_in_api_gateway_event(self): - # API Gateway sends null for headers, multiValueHeaders and queryStringParameters - # when the request has none. These used to crash before the request was dispatched. app = App(client=self.web_client, signing_secret=self.signing_secret) event = { "httpMethod": "POST", @@ -85,17 +83,13 @@ def test_null_fields_in_api_gateway_event(self): "isBase64Encoded": False, } response = SlackRequestHandler(app).handle(event, self.context) - # No signature headers, so the request is rejected rather than crashing assert response["statusCode"] == 401 - # The caller's event must not be modified assert event["headers"] is None def test_missing_is_base64_encoded_and_null_body(self): app = App(client=self.web_client, signing_secret=self.signing_secret) - # isBase64Encoded is absent (for example when invoked from a test tool) event = {"requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": "{}"} assert SlackRequestHandler(app).handle(event, self.context)["statusCode"] == 401 - # isBase64Encoded is true but the body is null event = {"requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": None, "isBase64Encoded": True} assert SlackRequestHandler(app).handle(event, self.context)["statusCode"] == 401