Coverage for gco / services / request_context.py: 100.00%
17 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1"""Per-request correlation ids for the manifest API.
3The jobs routes (and the other manifest API routers) return a constant,
4generic 500 detail so exception text never reaches a client — which leaves
5an operator who receives "I got a 500" with nothing to grep for. This module
6gives every request a server-generated correlation id that appears in three
7places at once:
9* the ``X-Request-ID`` response header (every response, success or error);
10* the generic 500 detail (``Internal server error (request-id: <id>)``);
11* the paired server-side error log line.
13The flow: a client reports the request id from the response, the operator
14greps the service logs for it, and lands directly on the logged exception.
16Ids are ALWAYS generated server-side (``uuid4().hex``) and never read from
17an inbound header: a client-controlled value that ends up next to log lines
18would need CWE-117 sanitization and could be replayed across requests to
19muddy an investigation. Hex-only ids are log-safe by construction.
21The id lives in a :class:`contextvars.ContextVar`, bound by the manifest
22API middleware for HTTP traffic. Code that runs outside the middleware
23(unit tests calling route coroutines directly) still gets a consistent id:
24:func:`current_request_id` binds a fresh one on first use, so the log line
25and the response detail produced in the same context always agree.
26"""
28from __future__ import annotations
30import uuid
31from contextvars import ContextVar, Token
33#: Response header carrying the correlation id on every manifest API response.
34REQUEST_ID_HEADER = "X-Request-ID"
36_REQUEST_ID: ContextVar[str | None] = ContextVar("gco_request_id", default=None)
39def new_request_id() -> str:
40 """A fresh server-generated correlation id (32 lowercase hex characters)."""
41 return uuid.uuid4().hex
44def bind_request_id() -> tuple[str, Token[str | None]]:
45 """Bind a fresh id to the current context; return it with its reset token."""
46 request_id = new_request_id()
47 return request_id, _REQUEST_ID.set(request_id)
50def unbind_request_id(token: Token[str | None]) -> None:
51 """Restore the context to its state before the matching :func:`bind_request_id`."""
52 _REQUEST_ID.reset(token)
55def current_request_id() -> str:
56 """The bound correlation id, binding a fresh one on first use.
58 Bind-on-first-use keeps the id stable for the remainder of the current
59 context, so an error handler that logs the id and then embeds it in the
60 response detail reports the same value in both places even when no
61 middleware ran (direct route invocation in tests).
62 """
63 request_id = _REQUEST_ID.get()
64 if request_id is None:
65 request_id, _ = bind_request_id()
66 return request_id