# 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.

Source: https://howhttpworks.com/debug/err-empty-response
Last reviewed: 2026-10-05

Error messages this page covers:
- `ERR_EMPTY_RESPONSE`
- `didn’t send any data.`
- `curl: (52) Empty reply from server`
- `Error: socket hang up`
- `ECONNRESET`
- `Remote end closed connection without response`
- `upstream prematurely closed connection while reading response header from upstream`

> **TL;DR:** Run `curl -v --http1.1 https://example.com/failing-path` and 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`](https://github.com/chromium/chromium/blob/main/net/base/net_error_list.h) as a connection closed without sending data. Its [English error page](https://github.com/chromium/chromium/blob/main/components/error_page_strings.grdp) says the hostname **didn’t send any data.** curl's [HTTP completion check](https://github.com/curl/curl/blob/master/lib/http.c) 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:

```text
curl: (52) Empty reply from server
```

This is not an HTTP status code. Under [HTTP/1.1 framing](https://www.rfc-editor.org/rfc/rfc9112#section-2.1), 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](https://howhttpworks.com/debug/err-connection-refused); a reset can instead produce [ERR_CONNECTION_RESET](https://howhttpworks.com/debug/err-connection-reset) or a curl receive error.

[Node's HTTP client](https://nodejs.org/api/http.html) 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`](https://docs.python.org/3/library/http.client.html#http.client.RemoteDisconnected) corresponds to reading no response data; its [source message](https://github.com/python/cpython/blob/main/Lib/http/client.py) 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:

```bash
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`](https://curl.se/docs/manpage.html#--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`](https://man.openbsd.org/nc) lets you type a minimal request:

```bash
nc -v example.com 80
```

Type these lines and press Enter twice after the last header:

```http
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:

```bash
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:

```bash
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](https://docs.docker.com/engine/containers/resource_constraints/): 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:

```bash
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`](https://github.com/nginx/nginx/blob/master/src/http/ngx_http_request.c) and [maps its internal error to 400](https://github.com/nginx/nginx/blob/master/src/http/ngx_http_special_response.c). 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](https://howhttpworks.com/debug/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:

```nginx
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](https://github.com/nginx/nginx/blob/master/src/http/ngx_http_upstream.c) 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](https://howhttpworks.com/debug/nginx-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](https://docs.docker.com/engine/network/port-publishing/) forwards traffic to a container port. It does not start the application or make a loopback-only listener reachable:

```bash
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:

```bash
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](https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback), 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`](https://nginx.org/en/docs/http/ngx_http_rewrite_module.html#return) closes the connection without response headers. A default virtual host or security rule may select it when Host or another request condition fails:

```bash
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:

```bash
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](https://howhttpworks.com/status-codes/444).

### 5. A pooled connection is reused as the server closes it

[RFC 9112 Section 9.5](https://www.rfc-editor.org/rfc/rfc9112#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`](https://nodejs.org/api/http.html#requestreusedsocket).

Compare repeated URLs in one curl process with separate fresh processes:

```bash
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](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2) 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](https://www.rfc-editor.org/rfc/rfc9112#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:

```bash
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](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/edit-load-balancer-attributes.html#desync-mitigation-mode), 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](https://howhttpworks.com/status-codes/444): intentional connection closure without headers.
- [ERR_CONNECTION_RESET](https://howhttpworks.com/debug/err-connection-reset): the peer resets rather than completing the exchange.
- [ERR_CONNECTION_REFUSED](https://howhttpworks.com/debug/err-connection-refused): no accepted connection on the target address and port.
- [nginx 502 Bad Gateway](https://howhttpworks.com/debug/nginx-502-bad-gateway): the proxy reports an upstream failure with an HTTP response.
