Version
amphp/http-server v3.4.6 (via pestphp/pest-plugin-browser), PHP 8.4, Linux.
Problem
Http1Driver::write() decides on chunked encoding with
$chunked = !$shouldClose
&& (!isset($headers["content-length"]) || $trailers !== null)
&& $protocol === "1.1"
&& $status >= HttpStatus::OK;
A 204 No Content (or 304 Not Modified) response that carries no Content-Length header is therefore sent as
HTTP/1.1 204 No Content
transfer-encoding: chunked
...
0\r\n\r\n
RFC 7230 §3.3.1 and §3.3.3: a server MUST NOT send a Transfer-Encoding header field in any response with a status code of 1xx or 204, and 204/304 responses have no message body regardless of header fields. Clients follow the RFC: Chromium treats the 204 as complete after the headers and returns the keep-alive socket to its pool with the five bytes 0\r\n\r\n still unread. The next request on that socket then fails with net::ERR_INVALID_HTTP_RESPONSE because the parser reads the stray chunk terminator before the status line.
How it was found
A Laravel app served through pest-plugin-browser's in-process server (which uses this package) fires POST /cookie-consent/impression → 204 early in page load; when Chromium reused that socket for the next script request, the script silently failed and the page never booted. Confirmed with a Chromium NetLog (SOCKET_BYTES_RECEIVED byte_count: 5 after the 204, then HTTP_STREAM_PARSER_READ_HEADERS net_error: -370 on the following request) and with tcpdump on the loopback interface. Setting Content-Length: 0 on the 204 makes the driver skip the body and the problem disappears.
Expected
Skip the body and never emit Transfer-Encoding for 1xx/204/304 the way HEAD is already special-cased in the same method; treating an explicit Content-Length: 0 from the handler as an opt-out is not enough because frameworks (Symfony HttpFoundation, for one) strip Content-Length from those responses on purpose.
Happy to send a PR if you agree with the shape.
Version
amphp/http-server v3.4.6 (via pestphp/pest-plugin-browser), PHP 8.4, Linux.
Problem
Http1Driver::write()decides on chunked encoding withA
204 No Content(or304 Not Modified) response that carries noContent-Lengthheader is therefore sent asRFC 7230 §3.3.1 and §3.3.3: a server MUST NOT send a
Transfer-Encodingheader field in any response with a status code of 1xx or 204, and 204/304 responses have no message body regardless of header fields. Clients follow the RFC: Chromium treats the 204 as complete after the headers and returns the keep-alive socket to its pool with the five bytes0\r\n\r\nstill unread. The next request on that socket then fails withnet::ERR_INVALID_HTTP_RESPONSEbecause the parser reads the stray chunk terminator before the status line.How it was found
A Laravel app served through pest-plugin-browser's in-process server (which uses this package) fires
POST /cookie-consent/impression→204early in page load; when Chromium reused that socket for the next script request, the script silently failed and the page never booted. Confirmed with a Chromium NetLog (SOCKET_BYTES_RECEIVED byte_count: 5after the 204, thenHTTP_STREAM_PARSER_READ_HEADERS net_error: -370on the following request) and with tcpdump on the loopback interface. SettingContent-Length: 0on the 204 makes the driver skip the body and the problem disappears.Expected
Skip the body and never emit
Transfer-Encodingfor 1xx/204/304 the way HEAD is already special-cased in the same method; treating an explicitContent-Length: 0from the handler as an opt-out is not enough because frameworks (Symfony HttpFoundation, for one) stripContent-Lengthfrom those responses on purpose.Happy to send a PR if you agree with the shape.