Cuts release 0.20.0. ## Highlights - X-Request-ID header on every response + sanitized to [A-Za-z0-9_-] (CRLF log-forging mitigation) - Error pages (HTML + JSON 500) surface request_id for support tickets - Dev debug toolbar gated by DEBUG=1 — fastapi-debug-toolbar with custom DuckDBPanel - Centralized app.logging_config.setup_logging() replaces 23 scattered basicConfig calls - Telegram bot drops bot.log file — stdout only (BREAKING) ## Devin findings addressed - BUG_0001: .env.template no longer claims FastAPI debug=True - BUG_0002: subprocess extractor logs INFO to stderr again - ANALYSIS_0003: _wants_html no longer matches Accept: */* (curl gets JSON as before) - BUG on b1c6ee9: HTML 500 page no longer leaks str(exc) in production - BUG on b13d2fe: 2 CLAUDE.md compliance flags (transform.py + ws_gateway) accepted as scope-limited logging refactor — follow-up to update CLAUDE.md if needed See CHANGELOG [0.20.0] for full notes.
50 lines
1.7 KiB
Python
50 lines
1.7 KiB
Python
"""Request-ID middleware. Assigns or propagates X-Request-ID per request.
|
|
|
|
Pure ASGI middleware (not BaseHTTPMiddleware) so the request_id ContextVar
|
|
propagates into route handlers and BackgroundTasks without being clobbered
|
|
by an early `finally`-block reset. Each request runs in its own asyncio
|
|
task with an isolated context copy, so no manual reset is needed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
|
|
from app.logging_config import request_id_var
|
|
|
|
_MAX_RID_LEN = 64
|
|
_ALLOWED = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
|
|
|
|
|
|
def _sanitize(rid: str) -> str:
|
|
cleaned = "".join(c for c in rid if c in _ALLOWED)[:_MAX_RID_LEN]
|
|
return cleaned or uuid.uuid4().hex[:12]
|
|
|
|
|
|
class RequestIdMiddleware:
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
self.app = app
|
|
|
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
if scope["type"] != "http":
|
|
await self.app(scope, receive, send)
|
|
return
|
|
|
|
rid_header: str | None = None
|
|
for k, v in scope.get("headers", []):
|
|
if k == b"x-request-id":
|
|
rid_header = v.decode("latin-1", errors="replace")
|
|
break
|
|
rid = _sanitize(rid_header) if rid_header else uuid.uuid4().hex[:12]
|
|
request_id_var.set(rid)
|
|
|
|
async def send_wrapper(message: Message) -> None:
|
|
if message["type"] == "http.response.start":
|
|
headers = list(message.get("headers", []))
|
|
headers.append((b"x-request-id", rid.encode("latin-1")))
|
|
message["headers"] = headers
|
|
await send(message)
|
|
|
|
await self.app(scope, receive, send_wrapper)
|