fix: 请求处理未捕获异常回 500 JSON,不再断连变 nginx 502

2026-10-27 死锁事件的次生问题:_dispatch 里未捕获异常逃出
handle_one_request 后连接无响应,前端只看到 nginx 502、拿不到
任何可读错误;死锁本身修掉后,同类内部异常仍会以 502 露出来。

兜底(_dispatch_safe,四个 do_* 方法统一走它):
- 任何异常先记 _server_log.exception(堆栈进服务端日志),
  再回固定 {"error": "internal_error"},不泄漏堆栈给客户端;
- _send_json/_send_html/SSE 发头后即标记已响应,之后异常
  只记日志、不二次发送(响应一旦开始不能再发状态行);
- _responded 每请求重置,同一连接 keep-alive 复用不串请求。

单测:异常请求得 500 JSON(非断连)+ 服务端记日志;
同一连接上 500 之后再请求仍 200。
This commit is contained in:
host committed 2026-10-27 10:56:08 +08:00
1 parent b0772d0eac
commit df8c493281
2 files changed
+142 -4

No files matched your search

+34 -4
View File
@@ -959,6 +959,9 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
# 发头即算已响应:之后 wfile 写失败(客户端已断)也不应再
# 尝试回 500——响应一旦开始就不能再发第二个状态行。
self._responded = True
self.wfile.write(body)
def _send_html(self, status, html_body):
@@ -967,6 +970,7 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self._responded = True
self.wfile.write(body)
def _read_body(self):
@@ -1006,18 +1010,43 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
# -- 入口 ----------------------------------------------------------
def do_GET(self):
self._dispatch("GET")
self._dispatch_safe("GET")
def do_POST(self):
self._dispatch("POST")
self._dispatch_safe("POST")
def do_DELETE(self):
self._dispatch("DELETE")
self._dispatch_safe("DELETE")
def do_PATCH(self):
# PATCH 之前没有入口(BaseHTTPRequestHandler 默认回 501),
# 项目补仓库是第一个 PATCH 路由;与其它方法一样走 _dispatch。
self._dispatch("PATCH")
self._dispatch_safe("PATCH")
def _dispatch_safe(self, method):
"""分发兜底:_dispatch 里任何未捕获异常都回 500 JSON,而不是
让请求线程直接断连(2026-10-27 死锁事件就是这么变成前端
“HTTP 502”的:异常逃出 handle_one_request 后连接无响应,
nginx 只能报 502,客户端拿不到任何可读错误)。
500 体只写固定 internal_error,不把堆栈泄漏给客户端;
堆栈走 _server_log.exception 进服务端日志。已开始响应
(SSE 流、_send_json/_send_html 已发头)时不再二次发送,
只记日志——响应一旦开始就不能再发第二个状态行。
_responded 每请求重置(同一连接 keep-alive 复用 handler
对象时不串请求)。
"""
self._responded = False
try:
self._dispatch(method)
except Exception:
_server_log.exception("unhandled error in %s %s",
method, self.path)
if not self._responded:
try:
self._send_json(500, {"error": "internal_error"})
except Exception:
pass
def _dispatch(self, method):
parsed = urlparse(self.path)
@@ -2461,6 +2490,7 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
self.end_headers()
except (BrokenPipeError, ConnectionResetError):
return
self._responded = True
# SSE 无 Content-Length,done 后靠关连接界定流结束;注意
# send_header("Connection", "keep-alive") 会把
# close_connection 改回 False,所以必须在 end_headers 之后