"""request_id 关联中间件(ARCH §9.3 / CLAUDE.md)。 每请求生成/透传 request_id,绑入 structlog contextvars,并回写响应头, 错误信封也带上同一 id,便于端到端 grep。 """ from __future__ import annotations import re import uuid from collections.abc import Awaitable, Callable import structlog from starlette.requests import Request from starlette.responses import Response REQUEST_ID_HEADER = "x-request-id" # 放行安全字符集(字母数字 . _ -,1–128 位):覆盖 uuid4().hex、常见 trace id 与客户端 # 自定义短 id;含空白/控制字符/换行/注入序列/超长的一律丢弃改生成新 uuid——防止把未经 # 校验的客户端值原样写进日志/响应头(日志注入防护)。 _REQUEST_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,128}$") def _sanitize_request_id(raw: str | None) -> str: if raw is not None and _REQUEST_ID_PATTERN.match(raw): return raw return uuid.uuid4().hex async def request_id_middleware( request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: request_id = _sanitize_request_id(request.headers.get(REQUEST_ID_HEADER)) structlog.contextvars.clear_contextvars() structlog.contextvars.bind_contextvars(request_id=request_id) request.state.request_id = request_id response = await call_next(request) response.headers[REQUEST_ID_HEADER] = request_id return response