Skip to content

Bug: HttpResolverLocal deadlocks on RequestValidationError under uvicorn (sync middleware that raises before calling next) #8187

@avplab

Description

@avplab

Expected Behaviour

A POST request to an HttpResolverLocal route whose body fails pydantic validation (when enable_validation=True) should return HTTP 422 with the standard validation-error response.

Current Behaviour

The request hangs forever. The connection is accepted, the request body is received, but the response is never sent and no log line is emitted. Concurrent requests on the same uvicorn worker are unaffected, but the worker accumulates hung tasks (visible during shutdown as Waiting for background tasks to complete).

Code snippet

Minimal standalone repro — no custom exception handlers, no extra middleware:

# repro_app.py
from aws_lambda_powertools.event_handler import HttpResolverLocal
from pydantic import BaseModel, Field


class Body(BaseModel):
    items: list[str] = Field(min_length=1)


app = HttpResolverLocal(enable_validation=True)


@app.get("/ping")
def ping() -> dict[str, str]:
    return {"status": "ok"}


@app.post("/echo")
def echo(body: Body) -> dict[str, list[str]]:
    return {"items": body.items}

Run it:

uvicorn repro_app:app --host 0.0.0.0 --port 8090

Then:

# Works:
curl -s --max-time 5 http://localhost:8090/ping                                # → 200
curl -s --max-time 5 -X POST http://localhost:8090/echo \
  -H 'content-type: application/json' -d '{"items":["a"]}'                     # → 200

# Hangs forever:
curl -s --max-time 5 -X POST http://localhost:8090/echo \
  -H 'content-type: application/json' -d '{"items":[]}'                        # → timeout

Possible Solution

Root cause is in aws_lambda_powertools.event_handler.http_resolver._wrap_middleware_async.

The async coroutine awaits middleware_called_next.wait() to coordinate with the sync middleware running in a thread. When the sync middleware (e.g. OpenAPIValidationMiddleware) raises before calling sync_next — which is exactly what happens on RequestValidationError — the thread captures the exception in middleware_error_holder and exits. But the event is never set, so the awaiting coroutine waits forever.

Suggested fix — set the event in a finally block on run_middleware, and after await middleware_called_next.wait(), check whether next_app_holder is empty (i.e. the middleware errored without calling next) and propagate the captured exception immediately:

def run_middleware() -> None:
    try:
        middleware_result_holder.append(middleware(app, sync_next))
    except Exception as e:
        middleware_error_holder.append(e)
    finally:
        loop.call_soon_threadsafe(middleware_called_next.set)

# ...

await middleware_called_next.wait()

if middleware_error_holder and not next_app_holder:
    thread.join()
    raise middleware_error_holder[0]

I confirmed this fix locally via monkey-patch — the empty-list request then returns HTTP 422 correctly through the framework's built-in validation-error path.

Steps to Reproduce

  1. Save the snippet above to repro_app.py.
  2. pip install "aws-lambda-powertools[all]==3.28.0" pydantic uvicorn
  3. uvicorn repro_app:app --host 0.0.0.0 --port 8090
  4. curl --max-time 5 -X POST http://localhost:8090/echo -H 'content-type: application/json' -d '{"items":[]}' — observe the 5s timeout (HTTP 000).

The lambda invocation path (app.resolve(event, context)) is unaffected and returns 422 correctly — the deadlock is specific to the ASGI __call__ path used by uvicorn.

Powertools for AWS Lambda (Python) version

3.28.0 (also reproduced on 3.27.0)

AWS Lambda function runtime

N/A (local dev — issue is in HttpResolverLocal ASGI path, not Lambda)

Debugging logs

No application logs are emitted for the hung request — the deadlock occurs before the route handler runs. uvicorn logs only show the request never completing; on shutdown:

INFO:     Shutting down
INFO:     Waiting for background tasks to complete. (CTRL+C to force quit)

(Container/process must be force-killed to recover.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions