Merge pull request #76 from openqj/fix-handler-500
fix: 请求处理未捕获异常回 500 JSON,不再断连变 nginx 502
This commit is contained in:
2 files changed
+142
-4
No files matched your search
@@ -0,0 +1,108 @@
|
||||
"""请求处理 500 兜底单测。
|
||||
|
||||
_dispatch 里未捕获异常必须回 500 JSON,而不是让请求线程直接
|
||||
断连(2026-10-27 死锁事件就是这么变成前端“HTTP 502”的)。
|
||||
同时验证:500 之后同一连接仍可用(_responded 每请求重置),
|
||||
服务端记了异常日志。
|
||||
"""
|
||||
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
try:
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.server import CloudControlPlaneHandler, create_server
|
||||
except ImportError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from cloud.auth import AuthStore
|
||||
from cloud.server import CloudControlPlaneHandler, create_server
|
||||
|
||||
|
||||
class Handler500TestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||||
self.auth = AuthStore(self.db_path)
|
||||
self.owner_id, self.owner_key = self.auth.create_user("owner")
|
||||
self.server = create_server(self.db_path, "127.0.0.1", 0)
|
||||
self.port = self.server.server_address[1]
|
||||
self.thread = threading.Thread(
|
||||
target=self.server.serve_forever, daemon=True)
|
||||
self.thread.start()
|
||||
status, self.project = self._req(
|
||||
"POST", "/projects", {"name": "owner/repo"}, self.owner_key)
|
||||
self.assertEqual(status, 201, self.project)
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.thread.join(timeout=5)
|
||||
try:
|
||||
self.auth.conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.tmp.cleanup()
|
||||
|
||||
def _req(self, method, path, body=None, key=None, conn=None):
|
||||
own = conn is None
|
||||
if own:
|
||||
conn = http.client.HTTPConnection(
|
||||
"127.0.0.1", self.port, timeout=10)
|
||||
hdrs = {}
|
||||
payload = None
|
||||
if body is not None:
|
||||
payload = json.dumps(body).encode("utf-8")
|
||||
hdrs["Content-Type"] = "application/json"
|
||||
if key is not None:
|
||||
hdrs["X-API-Key"] = key
|
||||
conn.request(method, path, body=payload, headers=hdrs)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read()
|
||||
if own:
|
||||
conn.close()
|
||||
try:
|
||||
parsed = json.loads(raw.decode("utf-8"))
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
parsed = None
|
||||
return resp.status, parsed
|
||||
|
||||
def test_exception_returns_500_json_not_dropped(self):
|
||||
# _dispatch 整体抛错:客户端必须拿到 500 + 固定 error 体,
|
||||
# 而不是连接断开(旧行为:无响应,nginx 转 502)。
|
||||
with mock.patch.object(
|
||||
CloudControlPlaneHandler, "_dispatch",
|
||||
side_effect=RuntimeError("boom")), \
|
||||
mock.patch("cloud.server._server_log") as log:
|
||||
status, body = self._req(
|
||||
"GET", "/projects", key=self.owner_key)
|
||||
self.assertEqual(status, 500)
|
||||
self.assertEqual(body, {"error": "internal_error"})
|
||||
log.exception.assert_called_once()
|
||||
|
||||
def test_connection_usable_after_500(self):
|
||||
# 同一连接上:先 500,再正常请求须 200——_responded
|
||||
# 必须每请求重置,不能把连接钉死在已响应状态。
|
||||
conn = http.client.HTTPConnection(
|
||||
"127.0.0.1", self.port, timeout=10)
|
||||
try:
|
||||
with mock.patch.object(
|
||||
CloudControlPlaneHandler, "_dispatch",
|
||||
side_effect=RuntimeError("boom")), \
|
||||
mock.patch("cloud.server._server_log"):
|
||||
status, body = self._req(
|
||||
"GET", "/projects", key=self.owner_key, conn=conn)
|
||||
self.assertEqual(status, 500)
|
||||
self.assertEqual(body, {"error": "internal_error"})
|
||||
status, body = self._req(
|
||||
"GET", "/projects", key=self.owner_key, conn=conn)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertIsInstance(body, dict)
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in new issue
Block a user