Skip to content

Bug: exception handlers hijack the default 404 and bypass _to_response (body null on ALB, dict/tuple crash) #8579

Closed

Description

@leandrodamascena

Expected Behaviour

  • An unmatched route returns 404 even when a broad @app.exception_handler(Exception) is registered.
  • Whatever an exception handler returns goes through the same _to_response adaptation as a route return value, so Response(body=None) on ALB becomes "" and a dict/tuple is converted or rejected clearly.

Current Behaviour

All in api_gateway.py, confirmed through a real API Gateway REST API:

  1. 404 hijacked. not_found_handler (line 2847) synthesizes a NotFoundError() that was never raised and looks it up with lookup_exception_handler, which walks the MRO. With @app.exception_handler(Exception), which the docs suggest as a catch-all, every unmatched route returns 500: GET /nope → 500 generic handler got NotFoundError().
  2. body: null on ALB. _call_exception_handler (line 2952) wraps handler(exp) directly in the response builder. On ALBResolver, a handler returning Response(status_code=400, body=None) emits "body": null and ALB answers 502. The same Response returned from a route emits "" because _to_response fixes it.
  3. dict/tuple return crashes. A handler returning {"error": str(exc)}, 400 raises AttributeError: 'tuple' object has no attribute 'is_json', masking the original exception. Real API: 502.

Code snippet

from aws_lambda_powertools.event_handler import ALBResolver, APIGatewayRestResolver, Response
from tests.functional.utils import load_event

ev = load_event("apiGatewayProxyEvent.json")  # GET /my/path

app = APIGatewayRestResolver()

@app.exception_handler(Exception)
def generic(exc):
    return Response(500, "text/plain", f"generic got {exc!r}")

@app.get("/other")
def other():
    return {}

print(app.resolve(ev, None))  # statusCode 500, body "generic got NotFoundError()"  (expected 404)

alb = ALBResolver()

@alb.exception_handler(ValueError)
def h(exc):
    return Response(status_code=400, content_type=None, body=None)

@alb.get("/lambda")
def r():
    raise ValueError("x")

print(alb.resolve(load_event("albEvent.json"), None)["body"])  # None  (expected "")

app2 = APIGatewayRestResolver()

@app2.exception_handler(ValueError)
def h2(exc):
    return {"error": str(exc)}, 400

@app2.get("/my/path")
def r2():
    raise ValueError("boom")

app2.resolve(ev, None)  # AttributeError: 'tuple' object has no attribute 'is_json'

Possible Solution

In not_found_handler, look up only an exact NotFoundError registration instead of walking the MRO. In _call_exception_handler, wrap the handler result with self._to_response(handler(exp)).

Steps to Reproduce

Run the snippet above with the tests/events fixtures. Full context in #8576.

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.13

Packaging format used

PyPi

Debugging logs

Warning

This issue is ready to be picked up, by humans or by agents. If you want it, comment here first so we can assign it to you, and please take only one or two issues at a time so others can join in too. Thanks!

Activity

  1. added this to the 3.38.0 milestone on Oct 18, 2026
  2. MrNK2107 commented on Oct 18, 2026

    @MrNK2107
    Contributor

    Hi @leandrodamascena, I'd like to work on this one. Plan:

    1. In not_found_handler, only use a handler registered for NotFoundError itself, so a catch-all @app.exception_handler(Exception) no longer turns unmatched routes into 500s.
    2. Route whatever an exception handler returns through _to_response, so Response(body=None) on ALB becomes "" and dict/tuple returns are adapted the same way as route return values.
    3. Regression tests for each of the three cases (REST + ALB).

    Could you assign it to me?

  3. leandrodamascena commented on Oct 18, 2026

    @leandrodamascena
    ContributorAuthor

    Hey @MrNK2107 pls go ahead

  4. added a commit that references this issue on Oct 18, 2026
    54e7e6f
  5. powertools-for-aws-oss-automation commented on Oct 22, 2026

    @powertools-for-aws-oss-automation

    Warning

    This issue is now closed. Please be mindful that future comments are hard for our team to see.
    If you need more assistance, please either reopen the issue, or open a new issue referencing this one.
    If you wish to keep having a conversation with other community members under this issue feel free to do so.

  6. added
    pending-releaseFix or implementation already in dev waiting to be released
    and removed
    triagePending triage from maintainers
    on Oct 22, 2026
  7. added a commit that references this issue on Oct 22, 2026
    781f60e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingevent_handlersgood first issueGood for newcomerspending-releaseFix or implementation already in dev waiting to be released

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions