ERR_EMPTY_RESPONSE / curl (52) Empty reply (tested)
net::ERR_EMPTY_RESPONSE in Chrome and curl: (52) Empty reply from server are the same event. The TCP connection opened, the request went out, and the server closed the connection without sending a single byte. We built eight server faults and recorded the exact error from Chromium, curl, Node fetch and Python for each one.
1. What the error means
The connection worked. The server accepted it and read the request. Then it closed the socket cleanly (a TCP FIN) before writing a status line. No status code exists, so there is nothing to show.
That rules out some things. If nothing listens on the port, you get ERR_CONNECTION_REFUSED. If the server closes with a TCP RST instead of a FIN, you get ERR_CONNECTION_RESET. If the server sends headers and then dies, you get a truncated-body error instead. We measured all three below.
2. Same event, four error messages
| Client | Exact error we saw |
|---|---|
| Chromium 153 | net::ERR_EMPTY_RESPONSE; page fetch() gives TypeError: Failed to fetch |
| curl 8.14.1 | curl: (52) Empty reply from server, exit 52, %{http_code} = 000 |
| Node 24 fetch | TypeError: fetch failed, cause.code = UND_ERR_SOCKET, cause message "other side closed" |
| Python urllib | http.client.RemoteDisconnected: Remote end closed connection without response |
In Node, "other side closed" with UND_ERR_SOCKET is the empty-reply signal. Many people search for socket hang up; that is what Node's older http.get API reported for the same server fault in our test (err.code ECONNRESET, message socket hang up), even though no TCP reset happened.
3. Results: cause by cause
| Server fault | Chromium | curl exit | Node fetch cause.code | Python |
|---|---|---|---|---|
req.socket.end() in handler, nothing written | ERR_EMPTY_RESPONSE | 52 | UND_ERR_SOCKET | RemoteDisconnected |
req.socket.destroy() in handler | ERR_EMPTY_RESPONSE | 52 | UND_ERR_SOCKET | RemoteDisconnected |
process.exit(1) in handler | ERR_EMPTY_RESPONSE | 52 | UND_ERR_SOCKET | RemoteDisconnected |
| Uncaught exception thrown in a timer (process crash) | ERR_EMPTY_RESPONSE | 52 | UND_ERR_SOCKET | RemoteDisconnected |
Express: res.write() then next(err) in the same tick | ERR_EMPTY_RESPONSE | 52 | UND_ERR_SOCKET | RemoteDisconnected |
server.timeout = 1000, handler takes 3 s | ERR_EMPTY_RESPONSE | 52 (after ~1 s) | UND_ERR_SOCKET | RemoteDisconnected (after ~1 s) |
http:// sent to an HTTPS (TLS) port | ERR_EMPTY_RESPONSE | 52 | UND_ERR_SOCKET | RemoteDisconnected |
| Look-alikes that are not empty replies | ||||
req.socket.resetAndDestroy() (TCP RST) | ERR_CONNECTION_RESET | 56 "Recv failure: Connection reset by peer" | ECONNRESET | ConnectionResetError 104 |
Express: res.flushHeaders() then next(err) | 200, then ERR_INCOMPLETE_CHUNKED_ENCODING | 18 "transfer closed with outstanding read data remaining" | 200, then body read throws TypeError: terminated | IncompleteRead(0 bytes read) |
| 20 KB request header (Node limit is 16 KB) | 431 with empty body | 0, status 431 | status 431 | HTTPError 431 |
| Express: error before anything is sent | Normal 500 page. Express handles it. | |||
The server log showed one request per client try. Chromium did not retry any of these on a fresh connection.
4. The surprising one: Express error after res.write()
If headers are already sent, Express cannot send a 500, so its final handler destroys the socket. We expected curl to get the headers and a truncated body. It did not. It got an empty reply:
app.get('/x', (req, res, next) => {
res.write('partial'); // res.headersSent is now true
next(new Error('boom')); // same tick
});
$ curl -sS http://127.0.0.1:8992/x
curl: (52) Empty reply from server
res.headersSent was true, but the bytes were still in Node's write buffer. Destroying the socket in the same tick threw them away. When we called next(err) one tick later (setImmediate), curl received the 200 headers and partial, then failed with exit 18. Either way the client gets a broken response. The fix is to not start writing until you know the response will succeed. See ERR_HTTP_HEADERS_SENT for the related "headers already sent" bug.
5. http:// to an HTTPS port
We sent plain HTTP to a Node https server. Every client reported an empty reply. The server logged:
tlsClientError ERR_SSL_HTTP_REQUEST ... SSL routines:tls_validate_record_header:http request
So if a URL gives ERR_EMPTY_RESPONSE, try https:// on the same port. The opposite mistake (https:// to a plain HTTP port) gives ERR_SSL_PROTOCOL_ERROR instead.
6. Timeouts that close silently
Node's server.timeout is 0 (off) by default in Node 24. When we set it to 1000 ms, a 3 s handler produced an empty reply after about 1 s in every client. The handler later found res.destroyed was true. Node 24 defaults we read from a fresh server: requestTimeout 300000 ms, headersTimeout 60000 ms, keepAliveTimeout 5000 ms, maxHeaderSize 16384 bytes.
Other layers do the same: a load balancer, a proxy or a platform (for example a serverless function hitting its time limit) may close the connection without a response. Not measured here: a proxy reusing a keep-alive connection that the backend just closed can also surface as an empty reply. Keep the backend keep-alive timeout longer than the proxy's.
7. Header too large is not an empty reply (in Node)
A 20 KB header got a real 431 Request Header Fields Too Large with Connection: close from Node 24, in all four clients. Chromium showed the 431 with an empty body, not ERR_EMPTY_RESPONSE. Other servers differ; see 431 Request Header Fields Too Large and Request Header Or Cookie Too Large.
8. How to fix it
- Reproduce with curl.
curl -v http://host/path. Exit 52 confirms an empty reply. Exit 56 means a reset; exit 18 means headers arrived but the body was cut. - Check the scheme. Try
https://on the same port. - Read the server log at that timestamp. A crash, restart or OOM kill shows up there. In our crash tests the process exited with status 1.
- Catch errors before writing. In Express, finish the work, then send. Do not call
res.write()before code that can throw. - Look for code that closes the socket. Search for
socket.destroy,socket.endandserver.timeout/setTimeouton the server. - Align timeouts. Each layer should wait longer than the one behind it.
Once the server responds, check the status and headers it sends with our HTTP header checker.
Frequently asked questions
What does ERR_EMPTY_RESPONSE mean?
The server accepted the connection and then closed it without sending any bytes. There is no status code. In our tests curl reported this as exit 52, "Empty reply from server".
Is ERR_EMPTY_RESPONSE the same as curl error 52?
Yes. In every case we built, Chromium 153 showed net::ERR_EMPTY_RESPONSE exactly when curl 8.14.1 exited with 52. Node fetch showed UND_ERR_SOCKET "other side closed" and Python showed RemoteDisconnected.
How is it different from ERR_CONNECTION_RESET?
A clean close (FIN) with no data gives ERR_EMPTY_RESPONSE and curl 52. A reset (RST) gives ERR_CONNECTION_RESET, curl 56 and Node ECONNRESET. We produced the reset with socket.resetAndDestroy().
Why do I get ERR_EMPTY_RESPONSE on localhost?
The most common causes we reproduced: the server process crashed in the handler, the code closed the socket, a server timeout fired, or you used http:// on a port that speaks HTTPS.
Does a too-large header cause an empty reply?
Not in Node 24. A 20 KB header got a proper 431 response with Connection: close in Chromium, curl, Node fetch and Python. Other servers may behave differently.
Written with AI assistance. Measured on 2026-10-02 with headless Chromium 153.0.8010.12 (Playwright), curl 8.14.1, Node.js 24.21.0 fetch and Python 3.13.5 urllib, against Node 24 and Express 5.2.1 test servers on 127.0.0.1. Proxy keep-alive races are from general docs, not measured. PlainHeaders is our extension; curl, Python, Node.js and Chromium are free.