From 7a42708545a06df27859776bda6fe14ad51dc413 Mon Sep 17 00:00:00 2001 From: Matteo Nastasi Date: Sun, 13 Sep 2026 13:31:15 +0200 Subject: [PATCH] fix the content codings announced on the stream Three inconsistencies between what the headers promised and what went out on the socket: - headers_render() announced "Content-Encoding: chunked" on every streaming response the client had not asked to compress. chunked is a transfer coding, not a content coding. Apache rewrote the response and it went unnoticed; nginx forwards the header as it stands, and a browser that meets a content coding it does not know refuses the whole body. Transfer-Encoding: chunked, right below, already says it. - the deflate stream was built without stating the window, and the filter then emits raw deflate, while Content-Encoding: deflate promises the zlib wrapper of RFC 1950. Whoever asked for deflate got bytes that could not be inflated as announced. - get_encoding() compared the tokens of Accept-Encoding as they came out of explode(), with their leading space and their quality attached. "gzip, deflate, br" only ever matched its first token, and a coding refused with q=0 was taken as accepted. Checked on the daemon socket, reading the raw stream and inflating it: gzip and deflate both come out whole across several chunks, every chunk self contained, the blocks paired and nothing duplicated; with no Accept-Encoding no Content-Encoding is sent at all. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE --- web/Obj/sac-a-push.phh | 29 +++++++++++++++++++++++++---- web/Obj/zlibstream.phh | 9 ++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/web/Obj/sac-a-push.phh b/web/Obj/sac-a-push.phh index 06b3c0b..393490b 100644 --- a/web/Obj/sac-a-push.phh +++ b/web/Obj/sac-a-push.phh @@ -342,9 +342,13 @@ function headers_render($header, $len) if (!isset($hset['expires'])) { $s .= "Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n"; } - if (!isset($hset['content-encoding'])) { - $s .= "Content-Encoding: chunked\r\n"; - } + /* No Content-Encoding here: "chunked" is not a content coding + but a transfer coding, and it used to be emitted whenever the + client asked for no compression. Apache rewrote the response and + it went unnoticed; nginx forwards it as it stands, and a browser + that meets a content coding it does not know refuses the whole + body (ERR_CONTENT_DECODING_FAILED). The line below states the + transfer coding, which is the one that is really in use. */ $s .= "Transfer-Encoding: chunked\r\n"; } $s .= $cookies; @@ -374,7 +378,24 @@ function get_encoding($header) { $enc = "plain"; if (isset($header['Accept-Encoding'])) { - $acc = explode(',', $header['Accept-Encoding']); + /* "gzip, deflate, br" splits into tokens that carry a leading space + and may carry a quality: comparing them as they come made every + coding but the first one unreachable, and let through a coding the + client had refused with q=0. */ + $acc = array(); + foreach (explode(',', $header['Accept-Encoding']) as $tok) { + $par = explode(';', $tok); + $name = strtolower(trim($par[0])); + $qual = 1.0; + for ($i = 1 ; $i < count($par) ; $i++) { + if (preg_match('/^\s*q\s*=\s*([0-9.]+)\s*$/', $par[$i], $m)) { + $qual = floatval($m[1]); + } + } + if ($qual > 0.0) { + $acc[] = $name; + } + } if (array_search('gzip', $acc) !== FALSE) { $enc = 'gzip'; diff --git a/web/Obj/zlibstream.phh b/web/Obj/zlibstream.phh index cd5b583..2fc927f 100644 --- a/web/Obj/zlibstream.phh +++ b/web/Obj/zlibstream.phh @@ -37,7 +37,14 @@ class ZLibStream { $thiz->head = "\037\213\010\000\000\000\000\000\000\003"; } else if ($type == 'deflate') { - if (($thiz->filter = stream_filter_append($thiz->s[1], "zlib.deflate", STREAM_FILTER_READ)) == FALSE) { + /* The window has to be stated: left to itself the filter emits a + raw deflate stream (no 78 9c header), while Content-Encoding: + deflate promises the zlib wrapper of RFC 1950. A positive window + asks for that wrapper; gzip above asks for the raw stream with a + negative one, and prepends its own header by hand. */ + $params = array('level' => 6, 'window' => 15, 'memory' => 9); + + if (($thiz->filter = stream_filter_append($thiz->s[1], "zlib.deflate", STREAM_FILTER_READ, $params)) == FALSE) { return (FALSE); } } -- 2.47.3