From 89154edf8aeef6644bd41372e66fe0ca5c27c050 Mon Sep 17 00:00:00 2001 From: SymbolStar Date: Wed, 15 Jul 2026 09:55:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(refs):=20CJK=20=E6=A0=87=E7=AD=BE=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=20/api/refs//content=20=E6=96=AD=E8=BF=9E=20(curl?= =?UTF-8?q?=2052)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit http.server 的 send_header 只允许 latin-1 header 值。ref 的 label / source_agent 里带中文时,写 X-Ref-Label 会抛 UnicodeEncodeError, 连接直接被关掉——前端表现为 chip 一直转圈、curl 报 code 52 空返回。 修复:对 X-Ref-Label / X-Ref-Source-Agent 做 latin-1 探测,非 ASCII/latin-1 内容用 percent-encode 兜底再塞进 header。回归测试 用 CJK 文件名跑一遍 round trip。 --- server.py | 16 ++++++++++++++-- tests/test_refs.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index a9f91db..b0beb5c 100644 --- a/server.py +++ b/server.py @@ -1001,14 +1001,26 @@ def _refs_get_content(self, ref_id: str) -> None: self._json({"error": str(e) or "blocked"}, 403) return etag = forge_refs._compute_etag(data) + # Non-ASCII (CJK) header values crash http.server on send; percent- + # encode label + source_agent so the response actually reaches the + # client. Without this, python's BaseHTTPRequestHandler raises + # UnicodeEncodeError writing headers and drops the connection + # (curl exit 52 / "Empty reply from server"). + def _ascii(v: str) -> str: + from urllib.parse import quote + try: + v.encode("latin-1") + return v + except UnicodeEncodeError: + return quote(v, safe="") self.send_response(200) self.send_header("Content-Type", mime) self.send_header("Content-Length", str(len(data))) self.send_header("Cache-Control", "no-store") self.send_header("ETag", etag) self.send_header("X-Ref-Etag", etag) - self.send_header("X-Ref-Label", ref.get("label", "")) - self.send_header("X-Ref-Source-Agent", ref.get("source_agent", "")) + self.send_header("X-Ref-Label", _ascii(ref.get("label", ""))) + self.send_header("X-Ref-Source-Agent", _ascii(ref.get("source_agent", ""))) self.send_header("X-Ref-Id", ref.get("id", "")) self.end_headers() self.wfile.write(data) diff --git a/tests/test_refs.py b/tests/test_refs.py index b569bda..e5aeac8 100644 --- a/tests/test_refs.py +++ b/tests/test_refs.py @@ -335,6 +335,25 @@ def test_http_content_round_trip(server, tmp_path): assert headers.get("X-Ref-Source-Agent") == "milk" +def test_http_content_cjk_label_does_not_crash(server, tmp_path): + # Regression: labels with CJK / non-latin-1 chars used to blow up + # BaseHTTPRequestHandler when writing X-Ref-Label, killing the + # connection (curl exit 52 / "Empty reply from server"). + from urllib.parse import quote + base = server + note = tmp_path / "SWAT-\u4f7f\u7528\u6307\u5357.md" + note.write_text("# 你好\n", encoding="utf-8") + _, body, _ = _call("POST", f"{base}/api/refs", { + "label": note.name, "abs_path": str(note), "source_agent": "milk", + }) + rid = body["id"] + status, body, headers = _call("GET", f"{base}/api/refs/{rid}/content") + assert status == 200 + assert "你好" in (body if isinstance(body, str) else body.decode("utf-8")) + # Header value must be latin-1-safe; ours is percent-encoded. + assert headers.get("X-Ref-Label") == quote(note.name, safe="") + + def test_http_put_content_non_md_400(server, tmp_path): # v1: non-md rejected at validation layer -> 400. base = server