From 80b5553865a5aa1f6c51d3693da652900fe7dbd5 Mon Sep 17 00:00:00 2001 From: Peng Ding Date: Fri, 12 Jun 2026 15:41:35 -0500 Subject: [PATCH] fix(httpserver): use chunked transfer encoding for SSE streaming responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSE responses previously skipped Transfer-Encoding: chunked and relied on Connection: close to signal body end. While valid per HTTP/1.1 spec, this breaks intermediate proxies (notably Go's httputil.ReverseProxy used by NPS) that expect either Content-Length or chunked framing — they misparse the raw SSE data as chunked encoding, producing errors like "invalid byte in chunk length". Unify all StreamingResponse output to use chunked encoding, matching the behavior of production SSE servers (OpenAI, Anthropic APIs). Closes #100 --- httpserver/httpserver.py | 28 ++++++++++++---------------- manifest.json | 10 +++++----- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/httpserver/httpserver.py b/httpserver/httpserver.py index 1f431e1..d55df1f 100644 --- a/httpserver/httpserver.py +++ b/httpserver/httpserver.py @@ -1,5 +1,5 @@ # /// zerodep -# version = "0.1.0" +# version = "0.1.1" # deps = [] # tier = "subsystem" # category = "network" @@ -299,9 +299,10 @@ def __init__( class StreamingResponse: """HTTP response streamed from an async generator. - Writes chunks using ``Transfer-Encoding: chunked`` unless - ``content_type`` is ``text/event-stream`` (SSE), in which case raw - bytes are flushed directly for maximum compatibility with SSE clients. + All streaming responses use ``Transfer-Encoding: chunked`` for + maximum compatibility with reverse proxies and intermediaries. + SSE (``text/event-stream``) responses additionally set + ``Cache-Control: no-cache``. Args: generator: Async iterator yielding ``bytes`` or ``str`` chunks. @@ -330,9 +331,8 @@ async def _write(self, writer: asyncio.StreamWriter) -> None: is_sse = self.content_type.startswith("text/event-stream") self.headers["Content-Type"] = self.content_type - if not is_sse: - self.headers.setdefault("Transfer-Encoding", "chunked") - else: + self.headers.setdefault("Transfer-Encoding", "chunked") + if is_sse: self.headers.setdefault("Cache-Control", "no-cache") self.headers.setdefault("Date", _http_date()) self.headers.setdefault("Connection", "close") @@ -349,16 +349,12 @@ async def _write(self, writer: asyncio.StreamWriter) -> None: async for chunk in self._generator: if isinstance(chunk, str): chunk = chunk.encode("utf-8") - if is_sse: - writer.write(chunk) - else: - writer.write(f"{len(chunk):x}\r\n".encode("latin-1")) - writer.write(chunk) - writer.write(b"\r\n") - await writer.drain() - if not is_sse: - writer.write(b"0\r\n\r\n") + writer.write(f"{len(chunk):x}\r\n".encode("latin-1")) + writer.write(chunk) + writer.write(b"\r\n") await writer.drain() + writer.write(b"0\r\n\r\n") + await writer.drain() except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError): logger.debug("Client disconnected during streaming") finally: diff --git a/manifest.json b/manifest.json index a6ff741..34c7cf3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "version": "1", - "generated": "2026-06-12T19:01:08.872115+00:00", + "generated": "2026-06-12T20:41:21.760825+00:00", "modules": { "a2a": { "description": "A2A (Agent-to-Agent Protocol) - Zero-dependency Python implementation", @@ -93,7 +93,7 @@ ], "tier": "subsystem", "category": "config", - "last_updated": "2026-06-12T12:49:15-05:00", + "last_updated": "2026-06-12T14:01:49-05:00", "content_hash": "83e2fc106c73f02bf5d239538c0e5916dcd9b2cc147bf648ae16c49ff4e5f459" }, "depdetect": { @@ -175,12 +175,12 @@ "files": [ "httpserver/httpserver.py" ], - "version": "0.1.0", + "version": "0.1.1", "deps": [], "tier": "subsystem", "category": "network", "last_updated": "2026-05-02T17:34:53+08:00", - "content_hash": "b477e97b7f4f4149ebf7911719eef117061a0efc72599ff6dc0b76b215f0492b" + "content_hash": "f168c322d56180d14759db3160ac8bcaf5a72b07895205beb9fd16270fbb0eb6" }, "jsonrpc": { "description": "JSON-RPC 2.0 -- Zero-dependency Python implementation", @@ -215,7 +215,7 @@ "deps": [], "tier": "simple", "category": "serialization", - "last_updated": "2026-06-12T13:46:30-05:00", + "last_updated": "2026-06-12T14:01:49-05:00", "content_hash": "2bfc0d657c366c2e09cfcbbd433545172989f615d67debd2ca8f55284af37ddd" }, "llmstxt": {