W76:修假后端 keep-alive 下请求体缓存跨请求串用(设备实测「3 张图只投出 2 张」的真凶)

`protocol_version = "HTTP/1.1"` ⇒ 同一条 keep-alive 连接上的多个请求由**同一个 Handler
实例**依次处理(`handle_one_request` 循环),而上一轮给 `_read_body` 加的缓存
(`_body_read` / `_body_cache`)从不在请求之间重置。后果两条:

- 第 2 个请求拿到第 1 个请求的 body ⇒ `out/http.jsonl` 里的摘要记成上一份;
- 第 2 个请求自己那份 body **没人从 `rfile` 读走**,残留字节留在内核缓冲里,被下一个
  请求的请求行一并吃掉 ⇒ `BaseHTTPRequestHandler` 回
  `501 Unsupported method ('{"sessionId":...}POST')`。

W76 设备实测里这表现成「系统分享 3 张图,只投出 2 条消息」,很容易被误判成 App 批量
投递的缺陷。修法是在 `handle_one_request` 入口把缓存清掉,按请求隔离。

验证:
1. 重启假后端后,在同一条 keep-alive 连接上连发 3 个**不同 body** 的
   `POST /api/v1/chat`,三发全 202,`out/http.jsonl` 记下三个不同 body(同一 client port)。
2. 完整设备重放(DocumentsUI 选 3 张图 → 分享 → Cyrene → 发送)三发三 202,
   3 条独立消息落库。
This commit is contained in:
AskaEth committed 2026-10-26 23:27:04 +08:00
1 parent 8babd1d2bb
commit b35b1b53ab
1 file changed
+21 -1
+21 -1
View File
@@ -319,11 +319,17 @@ class JsonlLog:
def __init__(self, path: str):
self.path = path
self.lock = threading.Lock()
os.makedirs(os.path.dirname(path), exist_ok=True)
self.dir = os.path.dirname(path)
os.makedirs(self.dir, exist_ok=True)
def append(self, record: dict) -> None:
line = json.dumps(record, ensure_ascii=False)
with self.lock:
# 目录**每次写都补一次**:README 说「删掉 `out/` 即清空」,而人在服务跑着的时候删掉
# 它就再也不会自己回来——之后每一条请求都会在写日志时抛 FileNotFoundError,
# 表现成「假后端活着但什么都不回」(W76 实测踩到)。重建目录只是一次 stat,
# 代价可以忽略;让它自己长回来,比要求人先停服务再删要可靠。
os.makedirs(self.dir, exist_ok=True)
with open(self.path, "a", encoding="utf-8", newline="\n") as handle:
handle.write(line + "\n")
@@ -1084,6 +1090,20 @@ class Handler(BaseHTTPRequestHandler):
query = parse_qs(parts.query, keep_blank_values=True)
return parts.path, query
def handle_one_request(self):
"""每个请求进来时**先把 body 缓存清掉**。
`protocol_version` 是 HTTP/1.1,同一条 keep-alive 连接上的多个请求由**同一个 Handler 实例**
依次处理(`handle_one_request` 循环),所以 `_read_body` 的缓存必须按请求隔离。少了这一步,
第 2 个请求会拿到第 1 个请求的 body(摘要记成上一份),而它自己那份**根本没人从 `rfile` 读走**
——残留字节留在内核缓冲里,被下一个请求的请求行一并吃掉,`BaseHTTPRequestHandler` 于是回
`501 Unsupported method ('{"sessionId":...}POST')`。客户端表现成「3 张图只投出去 2 张」,
而真正的锅在这里。
"""
self._body_read = False
self._body_cache = b""
super().handle_one_request()
def _read_body(self):
"""读出请求体,**并缓存**:`_handle` 记录摘要时要读它,各 handler 里的
`_parse_json_body` 随后再读——不缓存的话第二次只会读到空串(body 只能读一次)。"""