diff --git a/README.md b/README.md index f56f62e..dd52f71 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,8 @@ NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 | `MAX_HTML_LENGTH` | integer | `200000` | 单次 HTML 最大字节数 | | `API_KEY` | string | 空 | 非空时启用 `X-API-Key` 鉴权 | | `ALLOW_UNSAFE_HTML` | boolean | `true` | 是否关闭 HTML 安全拦截;当前默认开启,便于执行 JS | +| `ENABLE_REQUEST_DEBUG_LOG` | boolean | `true` | 是否输出 422 请求调试日志 | +| `REQUEST_LOG_MAX_CHARS` | integer | `10000` | 单次请求日志最多记录多少字符 | ## 给腾讯云智能体的最小参数说明 diff --git a/backend/.env b/backend/.env index 3214aae..637be23 100644 --- a/backend/.env +++ b/backend/.env @@ -8,3 +8,5 @@ MAX_RETENTION_DAYS=30 MAX_HTML_LENGTH=200000 API_KEY="" ALLOW_UNSAFE_HTML=true +ENABLE_REQUEST_DEBUG_LOG=true +REQUEST_LOG_MAX_CHARS=10000 diff --git a/backend/.env.example b/backend/.env.example index 3214aae..637be23 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,3 +8,5 @@ MAX_RETENTION_DAYS=30 MAX_HTML_LENGTH=200000 API_KEY="" ALLOW_UNSAFE_HTML=true +ENABLE_REQUEST_DEBUG_LOG=true +REQUEST_LOG_MAX_CHARS=10000 diff --git a/backend/app/config.py b/backend/app/config.py index 3a63211..0ebc304 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -76,6 +76,8 @@ class Settings: max_html_length = max(1024, _get_int_env("MAX_HTML_LENGTH", 200_000)) api_key = os.getenv("API_KEY", "").strip() allow_unsafe_html = _get_bool_env("ALLOW_UNSAFE_HTML", False) + enable_request_debug_log = _get_bool_env("ENABLE_REQUEST_DEBUG_LOG", True) + request_log_max_chars = max(256, _get_int_env("REQUEST_LOG_MAX_CHARS", 10_000)) settings = Settings() diff --git a/backend/app/main.py b/backend/app/main.py index 7204d9c..3e6cb19 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,7 +1,9 @@ import logging -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from app.config import settings from app.database import Base, SessionLocal, engine, ensure_database_schema @@ -39,6 +41,46 @@ app.add_middleware( app.include_router(html.router, prefix=settings.api_prefix) +def _truncate_for_log(value: str) -> str: + limit = settings.request_log_max_chars + if len(value) <= limit: + return value + + return f"{value[:limit]}... [truncated {len(value) - limit} chars]" + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler( + request: Request, + exc: RequestValidationError, +) -> JSONResponse: + if settings.enable_request_debug_log: + try: + raw_body = (await request.body()).decode("utf-8", errors="replace") + except Exception as body_error: + raw_body = f"" + + interesting_headers = { + key: value + for key, value in request.headers.items() + if key.lower() in {"content-type", "content-length", "x-api-key", "user-agent"} + } + logger.error( + "422 validation error: method=%s path=%s query=%s headers=%s errors=%s body=%s", + request.method, + request.url.path, + dict(request.query_params), + interesting_headers, + exc.errors(), + _truncate_for_log(raw_body), + ) + + return JSONResponse( + status_code=422, + content={"detail": exc.errors()}, + ) + + @app.on_event("startup") def cleanup_on_startup() -> None: db = SessionLocal()