|
53 | 53 | } |
54 | 54 | _MAX_AI_STREAMS = int(os.getenv("BAZI_MAX_AI_STREAMS", "3")) |
55 | 55 | _AI_STREAM_SLOTS = threading.BoundedSemaphore(max(1, _MAX_AI_STREAMS)) |
| 56 | +_ALLOW_LEGACY_CHART_GET = os.getenv("BAZI_ALLOW_LEGACY_CHART_GET", "1").lower() in ("1", "true", "yes") |
56 | 57 | _FUSION_STREAM_TOTAL_TIMEOUT = float(os.getenv("BAZI_FUSION_STREAM_TOTAL_TIMEOUT", "150")) |
57 | 58 | _FUSION_STREAM_IDLE_TIMEOUT = float(os.getenv("BAZI_FUSION_STREAM_IDLE_TIMEOUT", "45")) |
58 | 59 | _STREAM_HEARTBEAT_INTERVAL = float(os.getenv("BAZI_STREAM_HEARTBEAT_INTERVAL", "15")) |
|
63 | 64 | ] |
64 | 65 |
|
65 | 66 |
|
| 67 | +class RequestBodyTooLargeError(Exception): |
| 68 | + """Raised while consuming a chunked request body over the configured limit.""" |
| 69 | + |
| 70 | + |
66 | 71 | class ChartStreamRequest(BaseModel): |
67 | 72 | name: str = Field(default="", max_length=40) |
68 | 73 | gender: Literal["男", "女"] |
@@ -114,10 +119,31 @@ async def reject_large_request_bodies(request: Request, call_next): |
114 | 119 | if request.method in {"POST", "PUT", "PATCH"}: |
115 | 120 | content_length = request.headers.get("content-length") |
116 | 121 | max_bytes = _LARGE_REQUEST_BODY_LIMITS.get(request.url.path, _MAX_REQUEST_BODY_BYTES) |
117 | | - if content_length and int(content_length) > max_bytes: |
118 | | - response = JSONResponse({"error": "请求体过大"}, status_code=413) |
| 122 | + if content_length: |
| 123 | + try: |
| 124 | + if int(content_length) > max_bytes: |
| 125 | + response = JSONResponse({"error": "请求体过大"}, status_code=413) |
| 126 | + except ValueError: |
| 127 | + response = JSONResponse({"error": "无效的请求体长度"}, status_code=400) |
| 128 | + if response is None: |
| 129 | + receive = request._receive |
| 130 | + received_bytes = 0 |
| 131 | + |
| 132 | + async def limited_receive(): |
| 133 | + nonlocal received_bytes |
| 134 | + message = await receive() |
| 135 | + if message["type"] == "http.request": |
| 136 | + received_bytes += len(message.get("body", b"")) |
| 137 | + if received_bytes > max_bytes: |
| 138 | + raise RequestBodyTooLargeError |
| 139 | + return message |
| 140 | + |
| 141 | + request._receive = limited_receive |
119 | 142 | if response is None: |
120 | | - response = await call_next(request) |
| 143 | + try: |
| 144 | + response = await call_next(request) |
| 145 | + except RequestBodyTooLargeError: |
| 146 | + response = JSONResponse({"error": "请求体过大"}, status_code=413) |
121 | 147 | response.headers.setdefault("X-Content-Type-Options", "nosniff") |
122 | 148 | response.headers.setdefault("X-Frame-Options", "DENY") |
123 | 149 | response.headers.setdefault("Referrer-Policy", "no-referrer") |
@@ -176,6 +202,8 @@ def chart_api( |
176 | 202 | hour_confirmed: bool = Query(False, description="出生时辰是否经用户确认"), |
177 | 203 | practical: bool = Query(False, description="实用模式:仅返回白话解读,不包含技术推导"), |
178 | 204 | ): |
| 205 | + if not _ALLOW_LEGACY_CHART_GET: |
| 206 | + return JSONResponse({"error": "该入口已停用,请使用 POST /api/chart/stream"}, status_code=410) |
179 | 207 | ln_range = None |
180 | 208 | if liunian_from and liunian_to: |
181 | 209 | ln_range = (liunian_from, liunian_to) |
@@ -527,6 +555,9 @@ async def chart_stream( |
527 | 555 | data: {"phase":"done"} — 全流程结束 |
528 | 556 | """ |
529 | 557 |
|
| 558 | + if not _ALLOW_LEGACY_CHART_GET: |
| 559 | + return JSONResponse({"error": "该入口已停用,请使用 POST /api/chart/stream"}, status_code=410) |
| 560 | + logger.info("legacy chart stream GET used") |
530 | 561 | payload = ChartStreamRequest( |
531 | 562 | name=name, |
532 | 563 | gender=gender, |
|
0 commit comments