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

1"""Per-request correlation ids for the manifest API. 

2 

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: 

8 

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. 

12 

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. 

15 

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. 

20 

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""" 

27 

28from __future__ import annotations 

29 

30import uuid 

31from contextvars import ContextVar, Token 

32 

33#: Response header carrying the correlation id on every manifest API response. 

34REQUEST_ID_HEADER = "X-Request-ID" 

35 

36_REQUEST_ID: ContextVar[str | None] = ContextVar("gco_request_id", default=None) 

37 

38 

39def new_request_id() -> str: 

40 """A fresh server-generated correlation id (32 lowercase hex characters).""" 

41 return uuid.uuid4().hex 

42 

43 

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) 

48 

49 

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) 

53 

54 

55def current_request_id() -> str: 

56 """The bound correlation id, binding a fresh one on first use. 

57 

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