Debug guide · you're seeing
ERR_EMPTY_RESPONSEdidn’t send any data.curl: (52) Empty reply from serverError: socket hang upECONNRESETRemote end closed connection without responseupstream prematurely closed connection while reading response header from upstream
ERR_EMPTY_RESPONSE: Find Who Closed the Connection
Debug ERR_EMPTY_RESPONSE and curl empty replies by checking app crashes, wrong ports, nginx 444, Docker listeners, keep-alive races and request framing.
On this page
- What it means
- Establish where the reply disappears
- Find and fix the cause
- 1. The app crashes or is killed before writing
- 2. The proxy or client uses the wrong protocol or port
- 3. A Docker port is published, but nothing listens inside
- 4. nginx deliberately drops the request with return 444
- 5. A pooled connection is reused as the server closes it
- 6. Request framing triggers desync protection
- Verify the repair
- Related
TL;DR: Run
curl -v --http1.1 https://example.com/failing-pathand look for a connection followed by a sent request (>) but no response status line (< HTTP/...). Repeat directly against the app from the proxy host. If only the public route fails, inspect the proxy’s routing and drop rules; if the app also closes without replying, correlate its logs and restart/OOM evidence with the request time.
What it means
Chromium defines EMPTY_RESPONSE as a connection closed without sending data. Its English error page says the hostname didn’t send any data. curl’s HTTP completion check reports Empty reply from server when no response headers or body arrived.
For a direct HTTP/1.1 request, the server accepted the TCP connection and closed it without sending HTTP response bytes. With HTTPS, TLS handshake bytes may already have passed; “no data” here means no HTTP response, not literally no packets. If a proxy accepted the connection, the message does not prove the origin application received the request.
Illustrative curl output, verified against its source:
curl: (52) Empty reply from server
This is not an HTTP status code. Under HTTP/1.1 framing, even a response without a body still has a status line and header section. A 204 No Content or 200 OK with Content-Length: 0 is a valid response, not this error. A refused TCP connection belongs to ERR_CONNECTION_REFUSED; a reset can instead produce ERR_CONNECTION_RESET or a curl receive error.
Node’s HTTP client can emit Error: socket hang up with code ECONNRESET on premature closure. Those strings are broader than an empty reply and can also follow a locally aborted request. Python’s http.client.RemoteDisconnected corresponds to reading no response data; its source message is Remote end closed connection without response.
Establish where the reply disappears
Use the actual failing path, method and Host. The following examples use GET; substitute the original request when it is safe to repeat:
curl -v --http1.1 --max-time 15 https://example.com/failing-path -o /dev/null
# From the proxy host, if the app is an HTTP listener on port 3000:
curl --noproxy '*' -v --http1.1 --max-time 15 \
-H 'Host: example.com' http://127.0.0.1:3000/failing-path -o /dev/null
--http1.1 removes HTTP/2 negotiation from this comparison. If the forced HTTP/1.1 request works while the original protocol fails, investigate that protocol path rather than declaring the entire endpoint healthy. Verbose output can contain authorization headers and cookies; redact it before sharing.
For a cleartext listener, nc lets you type a minimal request:
nc -v example.com 80
Type these lines and press Enter twice after the last header:
GET / HTTP/1.1
Host: example.com
Connection: close
Interactive line endings depend on the terminal. For an exact request with CRLF terminators and a final blank line, use:
printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' \
| nc -w 5 example.com 80
nc connecting proves only that something accepted TCP. Use curl for HTTPS; plain nc does not perform a TLS handshake. A timeout waiting for a reply is also different from immediate closure.
Find and fix the cause
Check app health and routing first, then deliberate drops and reuse.
1. The app crashes or is killed before writing
If the app accepts a request and exits before writing headers, the client may see an empty reply or a reset. Look for an exception, worker exit or memory kill at the exact request time:
docker logs --timestamps --since 10m web
docker inspect --format \
'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} RestartCount={{.RestartCount}}' web
# Linux host or VM:
sudo journalctl -k --since '10 minutes ago' | grep -iE 'oom|out of memory|killed process'
sudo journalctl -u my-app --since '10 minutes ago'
Docker documents the kernel OOM killer: it can kill container processes under memory pressure. Exit code 137 alone is not proof of OOM; inspect the OOM state and host evidence. A restarted container’s current state is not a complete incident history. Run the kernel-log check on the Linux host or VM running the containers.
Fix the exception or memory pressure shown by the evidence. If a legitimate workload exceeds its measured allocation, change the container’s --memory limit in the deployment configuration and investigate growing memory use. Do not disable the OOM killer as an empty-response fix. For deploy-time failures, stop routing new requests to a terminating worker and allow in-flight requests to finish.
2. The proxy or client uses the wrong protocol or port
Probe the listener using the protocol it is configured to speak:
curl --noproxy '*' -v --http1.1 http://127.0.0.1:3000/
curl --noproxy '*' -v --http1.1 https://example.com:443/
sudo nginx -T 2>&1 | grep -E 'listen|server_name|proxy_pass|upstream'
HTTP sent to a TLS port: the listener can close without an HTTP reply, reset the connection, or send an error. nginx specifically logs client sent plain HTTP request to HTTPS port and maps its internal error to 400. So http://example.com:443/ is not guaranteed to produce curl 52. Use https://example.com/ for that listener.
TLS sent to a plain HTTP port: the client starts with TLS handshake bytes, not an HTTP request. A cleartext HTTP listener cannot complete that handshake; the result is a TLS negotiation error or connection closure, not a successful HTTPS response. This is a candidate for ERR_SSL_PROTOCOL_ERROR, not evidence that the response body is empty.
For an HTTP app on port 3000 behind nginx, a concrete routing configuration is:
location / {
proxy_pass http://127.0.0.1:3000;
}
Use https:// in proxy_pass only when the origin listener actually speaks TLS. If the app lives in another container, 127.0.0.1 points to nginx’s container; use the app’s service name and container port instead. Validate with nginx -t before reloading.
A missing upstream does not ordinarily explain a silent nginx response. Its upstream failure handling normally produces a gateway error if no retry succeeds. The verified log fragment upstream prematurely closed connection while reading response header from upstream identifies an app-side closure; the browser may receive 502 Bad Gateway from nginx. If the browser receives no response either, inspect the client-facing hop separately.
3. A Docker port is published, but nothing listens inside
Publishing a port forwards traffic to a container port. It does not start the application or make a loopback-only listener reachable:
docker port web
docker exec web sh -c 'ss -ltnp'
docker logs --timestamps --since 10m web
ss must be installed in the image; otherwise inspect the listener with tooling available in that container or its network namespace. If the app listens on port 3000, an example host mapping is:
docker run --name web -p 127.0.0.1:8080:3000 your-image
Bind the app to 0.0.0.0:3000 inside the container. For a Node server listening on an explicit address, that can be server.listen(3000, '0.0.0.0'). Test the host endpoint with curl -v http://127.0.0.1:8080/. A port forwarded to no listener can manifest as refusal, reset or an empty reply depending on the forwarding implementation. Diagnose the mapping and listener rather than relying on a particular error string.
4. nginx deliberately drops the request with return 444
return 444 closes the connection without response headers. A default virtual host or security rule may select it when Host or another request condition fails:
sudo nginx -T 2>&1 | grep -nE 'return[[:space:]]+444|default_server|server_name'
sudo tail -n 100 /var/log/nginx/access.log
sudo tail -n 100 /var/log/nginx/error.log
Replay the intended hostname, not just the server IP. For an HTTP listener:
curl --noproxy '*' -v -H 'Host: example.com' http://127.0.0.1:80/
An unmatched request reaching a return 444; block is expected to receive no HTTP response. Correct the request’s Host or the legitimate site’s server_name and routing. If a named route should reject with an explicit response, return 403; sends a status instead. Do not remove a deliberate default-host drop rule without identifying why the legitimate request reached it. See nginx 444.
5. A pooled connection is reused as the server closes it
RFC 9112 Section 9.5 describes this race: the server sees an idle connection while the client has begun another request. The resulting close or reset can happen before response headers arrive. Node documents it through request.reusedSocket.
Compare repeated URLs in one curl process with separate fresh processes:
curl -v --http1.1 -o /dev/null http://example.com/ -o /dev/null http://example.com/
curl -v --http1.1 -H 'Connection: close' http://example.com/ -o /dev/null
The first command permits reuse; the second starts a new connection and requests closure afterward. One successful run does not rule out an idle-time race. Reproduce the failing idle interval in the original client and log whether its socket was reused. With Node’s core HTTP client, http.get(url, { agent: false }, callback) is a focused comparison without a pooled agent.
Fix the pool’s idle lifetime relative to the server/proxy timeout and handle stale sockets. An empty reply does not prove the request was unprocessed. RFC 9110’s retry rules distinguish idempotent requests from operations that need application knowledge or deduplication before retrying.
6. Request framing triggers desync protection
If a simple GET works but a particular upload or generated request fails, compare its framing at each hop. Conflicting Content-Length and Transfer-Encoding, malformed lengths, or inconsistent parsing can trigger rejection and connection teardown. RFC 9112 Section 11.2 explains request smuggling; specific framing rules often require an error response followed by closure, not silent dropping.
Start with a clean control request:
curl -v --http1.1 https://example.com/upload -o /dev/null
# On nginx, correlate errors with the original failing request:
sudo tail -n 200 /var/log/nginx/error.log | grep -iE 'invalid|content.length|transfer.encoding'
Inspect the original client’s emitted headers and the proxy/security logs at the same time. For AWS ALB desync mitigation, check the access-log classification and reason: severe blocked requests receive 400, and certain ambiguous requests are routed with connections then closed. Connection teardown alone does not establish an empty reply or a smuggling attempt.
Fix the component emitting inconsistent framing and let the HTTP library calculate message lengths. Keep desync protections enabled; turning them off to suppress a connection error leaves the malformed request unresolved.
Verify the repair
Repeat the exact failing request through the public endpoint and directly at the app. Confirm the expected HTTP status and headers, then repeat under the condition that triggered the failure: a deploy, memory-heavy route or idle connection reuse. Correlate proxy and app logs so a successful health check cannot conceal a broken authenticated or upload path.
Related
- nginx 444: intentional connection closure without headers.
- ERR_CONNECTION_RESET: the peer resets rather than completing the exchange.
- ERR_CONNECTION_REFUSED: no accepted connection on the target address and port.
- nginx 502 Bad Gateway: the proxy reports an upstream failure with an HTTP response.
Frequently asked questions
What does ERR_EMPTY_RESPONSE mean?
Chrome received no response data before the connection closed. With a direct HTTP/1.1 connection, TCP can connect successfully while the peer sends no HTTP status line or headers. The closing peer may be a proxy rather than the application.
Is a 204 response an empty response error?
No. A 204 response has a status and headers even though it has no body. ERR_EMPTY_RESPONSE means the expected response never arrived.
Does HTTP on an HTTPS port always produce an empty reply?
No. A listener can close, reset, or reject the request with an HTTP response. nginx normally returns 400 for plain HTTP sent to an SSL listener. Probe the intended scheme and port rather than assuming one error string.
Why can a published Docker port still fail?
Publishing a port forwards traffic; it does not start a server inside the container. Verify the mapped container port and the application’s bind address. Depending on the forwarding path, a missing listener can produce a refusal, reset or empty reply.
Should I retry an empty reply?
An empty reply does not prove the server skipped the request. Retry only when the operation is safe to repeat or you have a way to deduplicate it. Diagnose idle connection reuse if fresh requests work but pooled requests fail.
Sources
- MDN: HTTP/1.x connection managementdeveloper.mozilla.org
- RFC 9112: message syntax, closure and request smugglingrfc-editor.org
- RFC 9110 Section 9.2.2: idempotent retriesrfc-editor.org
- Chromium: EMPTY_RESPONSE definitiongithub.com
- Chromium: browser error page wordinggithub.com
- curl: empty reply detectiongithub.com
- curl: command-line options and exit codescurl.se
- Node.js: premature close and reused socketsnodejs.org
- Node.js: listening on an explicit addressnodejs.org
- Python: RemoteDisconnecteddocs.python.org
- CPython: empty status-line handlinggithub.com
- nginx: return 444nginx.org
- nginx: upstream failure logging and handlinggithub.com
- nginx: plain HTTP on TLS port handlinggithub.com
- nginx: special responses map HTTP-to-HTTPS errors to 400github.com
- Docker: memory limits and OOM killsdocs.docker.com
- Docker: port publishingdocs.docker.com
- Docker: container logsdocs.docker.com
- Docker: container inspectiondocs.docker.com
- Docker Engine: container state fieldsgithub.com
- iproute2: ss listener inspectiongithub.com
- systemd: journalctl kernel, unit and time filtersgithub.com
- AWS ALB: desync mitigation behaviordocs.aws.amazon.com
- OpenBSD: nc manualman.openbsd.org
Related
ERR_CONNECTION_REFUSED: Localhost, Docker and Node Fixes
Fix ERR_CONNECTION_REFUSED by checking listeners, localhost IPv4/IPv6, Docker port mappings, firewall rejection, service crashes and Kubernetes endpoints.
ERR_CONNECTION_RESET and ECONNRESET: Find the RST
Fix ERR_CONNECTION_RESET, ECONNRESET and curl (56) Connection reset by peer: keep-alive races, body limits, firewalls, crashes. Find who sent the RST.
nginx 502 Bad Gateway: Causes and Fixes by Error Log
Fix nginx 502 Bad Gateway by matching the error log: connection refused, prematurely closed connection, php-fpm socket permissions, too big header, keepalive.
444 Connection Closed Without Response (nginx)
nginx 444 closes the connection without sending any response. Learn how to use return 444 to drop bots and unknown hosts, and what clients see.