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