diff --git a/reaction_board/web/server.py b/reaction_board/web/server.py index 0869b08..5de97c7 100644 --- a/reaction_board/web/server.py +++ b/reaction_board/web/server.py @@ -323,10 +323,9 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}} return path_value = _param(query, "path", "") - if not path_value: - body = self._read_small_body() - if body: - path_value = _json_path(body) + body = self._read_small_body() + if not path_value and body: + path_value = _json_path(body) user_agent = self.headers.get("User-Agent") if not track.count_bots and is_bot(user_agent): @@ -360,6 +359,8 @@ border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}} if length <= 0: return b"" if length > VISIT_BODY_LIMIT: + # 不读取超长请求体,直接关闭连接,避免 keep-alive 上残留字节错位。 + self.close_connection = True raise ApiError(HTTPStatus.BAD_REQUEST, "bad_request", "请求体过大") return self.rfile.read(length) diff --git a/tests/test_web_server.py b/tests/test_web_server.py index 030234a..7302535 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -270,6 +270,33 @@ def test_visit_endpoint_and_stats(): running.close() +def test_visit_query_path_drains_body(): + """path 也可来自查询串;请求体必须被读走,避免 keep-alive 连接错位。""" + with tempfile.TemporaryDirectory() as tmp: + running = RunningServer(tmp) + try: + ua = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/155.0"} + conn = http.client.HTTPConnection("127.0.0.1", running.port, timeout=5) + conn.request( + "POST", "/api/visit?path=/about", + body=json.dumps({"path": "/ignored"}), + headers={**ua, "Content-Type": "application/json"}, + ) + first = conn.getresponse() + first_payload = json.loads(first.read()) + assert first.status == 200 + assert first_payload["data"]["route"] == "/about" + + # 同一连接继续发请求:若上一次请求体未被读走,这里会解析错位 + conn.request("GET", "/api/health") + second = conn.getresponse() + health = json.loads(second.read()) + conn.close() + assert second.status == 200 and health["ok"] is True + finally: + running.close() + + def test_token_auth_and_login(): with tempfile.TemporaryDirectory() as tmp: running = RunningServer(tmp, token="test-token") @@ -385,6 +412,7 @@ def test_preview_and_loopback_guards(): def main_test(): test_readonly_routes_and_static() test_visit_endpoint_and_stats() + test_visit_query_path_drains_body() test_token_auth_and_login() test_preview_and_loopback_guards() print("OK: Web 只读路由/鉴权/预览/访问统计/安全边界测试全部通过")