"""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 JSONResponse, Response from ww_shared import ErrorBody, ErrorCode, ErrorEnvelope REQUEST_ID_HEADER = "x-request-id" # 请求体大小上限(2 MiB):超过即 413,防超大 body 撑爆内存/成本(CR-H9)。 MAX_BODY_BYTES = 2 * 1024 * 1024 # 放行安全字符集(字母数字 . _ -,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 async def body_size_limit_middleware( request: Request, call_next: Callable[[Request], Awaitable[Response]] ) -> Response: """按 `Content-Length` 拦截超大请求体,超限直接 413(CR-H9)。 用户中间件跑在 FastAPI 的 AppError 处理器**之外**,故必须自建错误信封响应(不能靠抛 AppError)。分块编码(无 `Content-Length`)无法在此拦截——单用户原型可接受的限制。 """ content_length = request.headers.get("content-length") if content_length is not None: try: size = int(content_length) except ValueError: size = 0 # 非法头防御性视作 0,交由下游正常解析/报错。 if size > MAX_BODY_BYTES: envelope = ErrorEnvelope( error=ErrorBody( code=ErrorCode.PAYLOAD_TOO_LARGE, message="请求体过大", request_id=_sanitize_request_id(request.headers.get(REQUEST_ID_HEADER)), ) ) return JSONResponse(status_code=413, content=envelope.model_dump()) return await call_next(request)