Skip to content

Add request-ID middleware that propagates X-Request-ID via ContextVar#45

Open
mvanhorn wants to merge 1 commit intoClimate-Vision:mainfrom
mvanhorn:request-id-middleware
Open

Add request-ID middleware that propagates X-Request-ID via ContextVar#45
mvanhorn wants to merge 1 commit intoClimate-Vision:mainfrom
mvanhorn:request-id-middleware

Conversation

@mvanhorn
Copy link
Copy Markdown

@mvanhorn mvanhorn commented May 5, 2026

Closes #44.

Why

AuditLogMiddleware already attaches a UUID to request.state.request_id, but anything that runs in the request lifecycle without access to the FastAPI Request object - notably inference/pipeline.py and the helper modules below it - emits log lines that aren't correlated to the inbound HTTP request.

What

Added in src/climatevision/api/middleware.py:

  • request_id_var: ContextVar[str | None] so non-FastAPI code can read the active request ID.
  • RequestIDMiddleware (BaseHTTPMiddleware) that reads X-Request-ID (UUID4 fallback), stores it on the ContextVar, mirrors it onto request.state.request_id so the existing RequestLoggingMiddleware / AuditLogMiddleware keep working, and echoes the value back on the response. ContextVar is reset() in a finally block so requests don't leak.
  • RequestIDLogFilter(logging.Filter) that exposes the ContextVar as record.request_id for log formatters.

setup_logging now installs the filter on the root logger and attaches it to existing handlers, and the JSON log format gains "request_id":"%(request_id)s".

create_app registers RequestIDMiddleware last in add_middleware, which means starlette runs it OUTERMOST -- the ContextVar is set before any other middleware or route handler runs.

Tests

tests/test_request_id_middleware.py covers the three cases in the issue plus a leak check:

  • request without X-Request-ID -> response has a UUID-shaped X-Request-ID and the route reads the same value
  • request with explicit X-Request-ID: <id> -> response echoes the same value back
  • log record emitted from a handler exposes record.request_id matching the inbound id (verified via the filter on a StreamHandler and pytest's caplog)
  • after a request finishes, request_id_var.get() is back to None

Locally:

PYTHONPATH=src python3 -m pytest tests/test_request_id_middleware.py -v

reports 4 passed in 0.10s. (The repo's full conftest pulls the inference pipeline which needs GDAL/fiona; the new test file builds a minimal FastAPI app of its own and stays independent.)

Closes Climate-Vision#44.

Adds a `RequestIDMiddleware` (in `src/climatevision/api/middleware.py`)
that reads the inbound `X-Request-ID` header, falls back to a fresh
UUID4 when absent, stores the value on a `contextvars.ContextVar`
(`request_id_var`), and echoes it back on the response.

Pairs the middleware with a `RequestIDLogFilter` and wires it into
`setup_logging` so every log record emitted during the request lifecycle
-- including from `inference/pipeline.py` and helper modules that don't
hold the FastAPI `Request` object -- carries `%(request_id)s` in the
JSON log format.

The middleware is registered last in `create_app` so it sits outermost
in the stack, ensuring the ContextVar is set before any other middleware
or route handler runs.

Tests in `tests/test_request_id_middleware.py` cover the three cases the
issue called out:

  - request without `X-Request-ID` -> response carries a UUID-shaped value
  - request with explicit `X-Request-ID` -> response echoes it back
  - log record emitted inside the handler exposes `record.request_id`

Plus a leak test asserting the ContextVar resets after the response.
@Hopelynconsult Hopelynconsult requested a review from femi23 May 7, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Good First Issue] Add request-ID middleware that propagates X-Request-ID through inference logs

1 participant