Repository navigation
Event Handler bug audit: 34 reproduced bugs grouped into 14 issues #8576
Closed
leandrodamascena
started this conversation in
General
Replies: 1 comment
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
I spent some time auditing the Event Handler utility looking for bugs. I read the code, wrote a reproduction for every suspect, ran all of them inside a real Lambda (Python 3.13,
developcode) and, for the ones that depend on the real event shape, through a real API Gateway REST API, HTTP API with a named stage, and an AppSync Events API. Everything below reproduces. None of these has an open issue yet.I grouped them by root cause so each group can become one issue and one PR. Severity is my call: High breaks a customer in normal use or bypasses validation, Medium contradicts the docs or HTTP semantics but has a workaround, Low is an edge case or only affects the generated schema.
I'll open individual issues from this list. If you hit any of these, or disagree with a severity, comment here.
High
1.
APIGatewayHttpResolverreturns 404 for every route when the stage is not$defaultand the path has no stage prefixutilities/data_classes/api_gateway_proxy_event.py:317APIGatewayProxyEventV2.pathdoesrawPath[len("/" + stage):]without checking thatrawPathstarts with the stage. With a custom domain API mapping (or SAM local)rawPathis/my/pathand stage isdev, sopathbecomespathand nothing matches.Note: I confirmed this in the Lambda runtime with the event shape AWS documents for custom domains, not through a live custom domain.
2. Body validation: multipart with non-ASCII text crashes, and a JSON array is accepted for an object parameter
middlewares/openapi_validation.py:221-241, 541, 800multipart/form-datawith a text field containingcaféraisesUnicodeDecodeError, which escapes becauseexcept ValueError: raisere-raises Unicode errors. Missingboundaryraises a bareValueError. Real API Gateway returns 502. Expected 422/415.[{"name":"a","price":1},{"name":"b","price":2}]sent todef create(item: Item)returns 200 with the first element. The list unwrapping meant for form values is applied to JSON bodies. Expected 422.Content-Dispositionwithfilename=beforename=is misparsed (find('name="')matches insidefilename="), giving 422 missing.3. Exception handlers hijack the default 404 and bypass
_to_responseapi_gateway.py:2847, 2952@app.exception_handler(Exception), which the docs suggest as a catch-all, turns every unmatched route into 500:not_found_handlersynthesizes aNotFoundError()and walks the MRO, so the generic handler receives it. Real API:GET /nope→500 generic handler got NotFoundError()._call_exception_handlerwrapshandler(exp)without_to_response. On ALB a handler returningResponse(body=None)emits"body": null(502 from ALB); the sameResponsereturned from a route emits"".AttributeError: 'tuple' object has no attribute 'is_json', masking the original exception. Real API: 502.Medium
4. AppSync Events:
UnauthorizedExceptionin a non-aggregate publish handler returns HTTP 200 and leaks the messageevents_appsync/appsync_events.py:257, 316The docs say raising
UnauthorizedExceptionfails the invocation and the publisher gets 403, and the documented example uses the defaultaggregate=False. In that mode the exception is caught per event. Real AppSync Events API:Events are not delivered in either case, so this is not a bypass. But the status code is wrong and the internal exception message reaches the publisher. Same for
async_on_publish.5. Route rules are compiled as unescaped regex, and regex
strip_prefixesreplaces every occurrenceapi_gateway.py:2778, 2809/items.jsonmatches/itemsXjson(200). A literal/a+breturns 404 for/a+b. Confirmed through a real REST API.strip_prefixes=[re.compile(r"/v[1-9]+")]on/v1/users/v2strips both, path becomes/users, 404.re.subhas nocount=1and no anchor.6. Header validation is inconsistent across resolvers
openapi_validation.py:144, 636, 644+ data classeslist[str]header with a single value → 422list_typeon HTTP API, Function URL, ALB and VPC Lattice v1. Two values work. REST v1 works. Real HTTP API confirmed. This is the docs exampleworking_with_headers_multi_value.pyon a different resolver.str→ 422 on REST v1 / ALB, but joined"a,b"on HTTP API.Content-Type: Application/JSON→ 415 on every resolver. Media types are case-insensitive.+jsonsuffixes are rejected too.7.
Responseand compression edge casesapi_gateway.py:364, 755, 838Response(content_type=APPLICATION_JSON, body=b'...')raisesTypeErrorwhenenable_validation=False. The docstring says bytes are base64 encoded.Response(headers={"content-type": "application/json"}, body={...})with lowercase header:is_json()misses it, the dict is returned unserialized, API Gateway returns 502.Accept-Encoding: gzip;q=0still compresses;GZIPdoes not. Confirmed through a real REST API.8. Async middleware: class-based
async def handlerleaks a coroutine,wrap_middleware_asyncraisesIndexErrormiddlewares/async_utils.py:101, 158BaseMiddlewareHandlersubclass withasync def handlerinresolve_asyncends withTypeError: Object of type coroutine is not JSON serializable. The docs say both class-based and async middlewares work.wrap_middleware_asyncwith a sync middleware that short-circuits (returns aResponsewithout calling next) raisesIndexError.9.
HttpResolverLocalcannot be used as a Lambda handler and crashes on binary bodieshttp_resolver.py:72, 270, 348handler = appas the docs example suggests fails withTypeError(the__call__is ASGI-only).app.resolve()with an HTTP API or Function URL event raisesKeyError: 'httpMethod'.UnicodeDecodeError.10. CORS response is missing
Vary: Originwhen echoing a specific allowed originapi_gateway.py:232With
allow_origin="https://a.com", extra_origins=["https://b.com"], the response carriesAccess-Control-Allow-Origin: https://a.comand noVary. A shared cache can serve origin A's response to origin B. Confirmed through a real HTTP API.Low
11. OpenAPI schema inaccuracies
openapi/schema_generator.py:373, 506,openapi/types.py:28,openapi/encoders.py:330,openapi_validation.py:422,api_gateway.py:1813responses={404: {"description": "nf"}}withoutcontentgets the handler's return model injected for every status code.Query():int | None,list[str]and Enum fields come out as{"type": "string"}.ValidationErrorcomponent requiresmsg, which is neither defined nor returned by the real 422 body.openapi_extensionsat root and operation level skip thex-prefix check thatServerenforces.Path(alias="ident")can never be satisfied (422 missing) and emits a parameter name not in the path template.Decimal("NaN")in a response raisesTypeErrorinstead ofSerializationError.12. Bedrock Agent: every event parameter passed as kwarg when validation is off,
nullcontent type keybedrock_agent.py:52, 350enable_validation=False, a query parameter not in the handler signature raisesTypeError: unexpected keyword argument.Responsewithout content type produces"responseBody": {"null": {...}}.13. AppSync: decorator returns
Noneon invalid path, context not cleared when an exception handler runsevents_appsync/_registry.py:56,appsync.py:160@app.on_publish("/default/*/sub")warns and returnsNone, so the decorated function disappears.AppSyncResolver.resolve()returns the exception handler result beforeclear_context(), soapp.contextleaks into the next invocation.14. Miscellaneous
api_gateway.py:3185,lambda_function_url.py:57,depends.py:121Routersilently overwrites a duplicate route (last wins); the resolver warns and keeps the first.LambdaFunctionUrlResolver._get_base_pathcheckshttp.methodinstead ofhttp.path. Unreachable today since Function URLs only use$default.Depends(SomeClass)ignores the__init__signature, so a class dependency cannot receiveRequestor a nestedDepends.All reactions