net::ERR_INVALID_RESPONSE (tested)
Chrome shows net::ERR_INVALID_RESPONSE or net::ERR_INVALID_HTTP_RESPONSE when it cannot make sense of what the server sent. We wrote a raw TCP server that sends broken responses on purpose and recorded the exact error each client gave. Most broken responses get a more specific error code, and many load without any error at all. If the server sends nothing at all, see ERR_EMPTY_RESPONSE.
1. The short answer
In our tests, Chromium 153 never returned ERR_INVALID_RESPONSE for a malformed HTTP/1.1 response. When the status line or header block was unreadable it returned ERR_INVALID_HTTP_RESPONSE. Other faults got their own codes: ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH, ERR_INVALID_CHUNKED_ENCODING, ERR_CONTENT_DECODING_FAILED, ERR_CONTENT_LENGTH_MISMATCH and others.
So if you see either name, do not guess. Look at the exact code in DevTools (Network tab, or the console line Failed to load resource: net::...), then find it in the table below. For fetch() the page only gets TypeError: Failed to fetch; the real code is only in the console. See TypeError: Failed to fetch.
2. Our test setup
A Node net.createServer read each request and wrote a fixed string back, then closed the socket. No HTTP library touched the response, so we could send anything:
import net from 'net';
const C = {
dupcl: 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Length: 3\r\n' +
'Connection: close\r\n\r\nhi!',
nulvalue: 'HTTP/1.1 200 OK\r\nX-Test: a\0b\r\nContent-Length: 2\r\n\r\nhi',
// ... 60 cases
};
net.createServer(s => s.on('data', d => {
const path = String(d).split(' ')[1].slice(1);
s.end(C[path]);
})).listen(8992, '127.0.0.1');
Chromium loaded each URL as a top-level navigation and as a same-origin fetch(). Both gave the same net error in every failing case. Then we ran the same cases through curl, Node fetch and Python urllib.request.
3. Chromium 153 results
| What the server sent | Chromium 153 result |
|---|---|
| No bytes, then close | net::ERR_EMPTY_RESPONSE |
Garbage instead of a status line (HELLO WORLD) | net::ERR_INVALID_HTTP_RESPONSE |
| Body with no status line at all (HTTP/0.9 style) | net::ERR_INVALID_HTTP_RESPONSE |
Text before the status line (Warning: something then HTTP/1.1 200 OK) | net::ERR_INVALID_HTTP_RESPONSE |
| NUL byte in a header value or header name | net::ERR_INVALID_HTTP_RESPONSE |
101 Switching Protocols to a normal request | net::ERR_INVALID_HTTP_RESPONSE |
103 Early Hints followed by garbage | net::ERR_INVALID_HTTP_RESPONSE |
Two Content-Length headers, different values (2 and 3) | net::ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH |
Two Content-Disposition headers, different values | net::ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION |
302 with two different Location headers | net::ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION |
302 with Location: http://[bad | net::ERR_INVALID_REDIRECT |
| One header of 300,000 bytes | net::ERR_RESPONSE_HEADERS_TOO_BIG |
Chunk size zz, or a 20-digit hex chunk size | net::ERR_INVALID_CHUNKED_ENCODING |
Chunked body cut off before the 0 chunk | net::ERR_INCOMPLETE_CHUNKED_ENCODING |
Content-Length: 50, only 2 bytes sent | net::ERR_CONTENT_LENGTH_MISMATCH |
Content-Encoding: gzip (or br, deflate, gzip, gzip) on a plain-text body | net::ERR_CONTENT_DECODING_FAILED |
407 from a server that is not a proxy | net::ERR_UNEXPECTED_PROXY_AUTH |
Only 100 Continue, then close | net::ERR_EMPTY_RESPONSE |
For the body errors (chunked, length, decoding) the navigation committed with status 200 and then failed. The headless page stayed blank and the load event never fired. fetch() rejected.
4. Broken responses Chromium accepted without error
These all loaded with no error in Chromium 153. Other clients were stricter (section 5).
- Status code not a number:
HTTP/1.1 abc OKloaded as 200. - Odd status codes:
0,99,600and1000were passed through as-is. HTTP/2.0 200 OK,HTTP/3 200 OK, lower-casehttp/1.1, a missing reason phrase, no space after the version (HTTP/1.1200).- Space before the colon (
X-Test : a), a header line with no colon, a bare CR inside a value, LF-only line endings, folded (obs-fold) header lines. - Two
Content-Lengthheaders with the same value. Content-Length: abc,Content-Length: -1and a 23-digit Content-Length: the body was read until close.Content-LengthandTransfer-Encoding: chunkedtogether: chunked won and the body was correct.- Unknown
Transfer-Encoding: foo, and unknownContent-Encoding: foo(body shown raw). Content-Encoding: gzipwith an empty body (Content-Length: 0).- 500 with an empty body: shown as a normal 500, not a net error.
- A 206 with a garbage or missing
Content-Range.
A 204 or 304 that carries a body made the navigation end in net::ERR_ABORTED (no new page). fetch() returned 204 or 304 with an empty body.
5. Same cases in curl, Node fetch and Python
| Case | curl 8.14.1 | Node 24 fetch (error.cause) | Python 3.13 urllib |
|---|---|---|---|
| Garbage status line | exit 1, "Received HTTP/0.9 when not allowed" | "Response does not match the HTTP/1.1 protocol (Expected HTTP/, RTSP/ or ICE/)" | BadStatusLine |
| Text before status line | exit 1, same HTTP/0.9 message | same as above | BadStatusLine |
HTTP/1.1 abc OK | exit 1, "Unsupported HTTP/1 subversion in response" | "(Invalid status code)" | BadStatusLine |
| Status 1000 | exit 8, "Invalid status line" | "(Invalid response status)" | BadStatusLine |
HTTP/2.0 status line | OK 200 | OK 200 | UnknownProtocol: HTTP/2.0 |
| Space before colon | OK 200 | "(Invalid header token)" | OK 200 |
| Header line with no colon | exit 8, "Header without colon" | "(Invalid header token)" | OK 200 |
| NUL in header value | exit 8, "Nul byte in header" | "(Invalid header value char)" | OK 200 |
| Bare CR in header value | OK 200 | "(Missing expected LF after header value)" | OK 200 |
| LF-only line endings | OK 200 | "(Missing expected CR after response line)" | OK 200 |
| Obs-fold header | OK 200 | "(Unexpected whitespace after header value)" | OK 200 |
| Two Content-Length, 2 and 3 | OK 200, read 3 bytes | UND_ERR_RES_CONTENT_LENGTH_MISMATCH | OK 200, read 2 bytes |
| Two Content-Length, both 2 | OK 200 | UND_ERR_RES_CONTENT_LENGTH_MISMATCH | OK 200 |
Content-Length: abc | exit 8, "Invalid Content-Length: value" | "(Invalid character in Content-Length)" | OK 200 |
| Content-Length + chunked | OK 200 | UND_ERR_RES_CONTENT_LENGTH_MISMATCH | OK 200 |
Chunk size zz | exit 56, "chunk hex-length char not a hex digit: 0x7a" | "terminated", cause "(Invalid character in chunk size)" | IncompleteRead(0 bytes read) |
| Fake gzip body | with --compressed: exit 61, "Error while processing content unencoding: incorrect header check" | "terminated", cause Z_DATA_ERROR "incorrect header check" | OK 200, raw bytes (urllib does not decode) |
| 101 to a normal GET | OK, code 101 | ERR_ASSERTION "assert(upgrade)" | HTTPError 101 |
| 300 KB header | exit 100, "A value or data field grew larger than allowed" | UND_ERR_HEADERS_OVERFLOW | LineTooLong (65536 bytes) |
The outer Node error is always TypeError: fetch failed (or terminated if the body broke). Print err.cause to see the real reason.
6. HEAD response with a body
We answered a HEAD request with Content-Length: 2 plus the 2 body bytes, and left the connection open. Chromium fetch, Node fetch and curl all returned 200 with an empty body and no error. None of them reused that connection: the next GET arrived on a new TCP connection (curl -v printed "shutting down connection #0"). So a stray HEAD body does not break clients we tested, but it costs a new connection every time. Fix it anyway: never write a body for HEAD.
7. Fixes
- ERR_INVALID_HTTP_RESPONSE on every page: something is writing bytes before the status line. Typical causes: a debug print or warning that reaches the socket before the headers, a plain-HTTP client talking to a TLS or non-HTTP port, or a proxy pointed at the wrong upstream port. Run
curl -v http://host/path; curl will say "Received HTTP/0.9 when not allowed" and you can see the first bytes. - NUL or control characters in a header: strip them before calling
setHeader/header(). Values built from user input or binary data are the usual source. Node's ownhttpmodule refuses them:res.setHeader('X-T', 'a\0b')threwERR_INVALID_CHAR"Invalid character in header content" in our test. So this usually comes from hand-built responses or another proxy. - ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH: two layers are each adding a length (app plus a middleware or proxy). Remove one. Details in Content-Length header mismatch.
- Content-Length and Transfer-Encoding together: Chromium tolerated it but Node fetch failed. Send one or the other. See Transfer-Encoding: chunked vs Content-Length.
- ERR_CONTENT_DECODING_FAILED: the header says gzip but the body is not gzip. Usually the body was compressed twice, or not at all, or a proxy decompressed it but kept the header. Check with
curl -sS -D - -o body.bin URLandfile body.bin. See Content-Encoding gzip vs br in nginx. - Multiple Location or Content-Disposition: make sure only one code path sets the header. Per the PHP docs (not measured),
header()replaces an earlier header of the same name by default; passingfalseas the second argument adds a second one. - Strange status codes: Chromium passed
0and1000through, but curl, Node and Python rejected them. Use 100 to 599.
If the connection is cut rather than malformed, see ERR_CONNECTION_RESET. For HTTP/2 framing errors, see ERR_HTTP2_PROTOCOL_ERROR.
Frequently asked questions
What is the difference between ERR_INVALID_RESPONSE and ERR_INVALID_HTTP_RESPONSE?
Both are Chromium network errors. In our tests with Chromium 153, every unreadable HTTP/1.1 status line or header block (garbage, text before the status line, NUL bytes in headers, a stray 101) gave ERR_INVALID_HTTP_RESPONSE. None of our 60 malformed responses produced ERR_INVALID_RESPONSE.
Does a space before the colon in a header break Chrome?
Not in Chromium 153: X-Test : a loaded fine, and so did curl and Python. Node 24 fetch rejected it with 'Invalid header token'. Fix it anyway, since stricter clients fail.
What happens with two Content-Length headers?
If the values differ, Chromium fails with net::ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH. If they are equal, Chromium loads the page, but Node 24 fetch still failed with UND_ERR_RES_CONTENT_LENGTH_MISMATCH.
Does Content-Encoding: gzip with a plain body give ERR_INVALID_RESPONSE?
No. Chromium 153 gave net::ERR_CONTENT_DECODING_FAILED, curl --compressed exited 61 with 'incorrect header check', and Node fetch failed with Z_DATA_ERROR.
How do I see what the server really sent?
Run curl -v against the URL. It prints the status line and headers it received and names the problem, for example 'Nul byte in header' or 'Received HTTP/0.9 when not allowed'. In Chrome, read the net:: code in the DevTools console.
Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright, navigation and fetch), Node.js 24.21.0 fetch, curl 8.14.1 and Python 3.13.5 urllib, against a raw Node net server on 127.0.0.1 that wrote 60 hand-crafted HTTP/1.1 responses byte by byte. PlainHeaders is our extension; curl, Python, Node.js and Chromium are free.