Debug guide · you're seeing
net::ERR_INCOMPLETE_CHUNKED_ENCODINGERR_INCOMPLETE_CHUNKED_ENCODINGcurl: (18) transfer closed with outstanding read data remainingECONNRESET: abortedIncompleteRead(5 bytes read)upstream prematurely closed connectionupstream timed out
ERR_INCOMPLETE_CHUNKED_ENCODING: Fix Truncated Responses
Fix ERR_INCOMPLETE_CHUNKED_ENCODING by finding missing final chunks, upstream crashes, nginx read timeouts, buffering failures and stalled SSE streams.
On this page
- What it means
- Find the hop that stopped sending
- Fix it, in diagnostic order
- 1. The upstream died after starting the body
- 2. A silent gap exceeded a proxy or load-balancer timeout
- 3. nginx cannot write its buffered response to disk
- 4. Compression buffered the stream or never finished
- 5. An inspecting proxy buffered or interrupted delivery
- Related
TL;DR: The connection closed before the response body finished. In HTTP/1.1 chunked encoding, a body is complete only when the final zero-length chunk arrives, so you can see
200 OKand still get this error. Runcurl --http1.1 -v --no-bufferagainst the failing route, then again directly against the upstream. If only the public route cuts off, look at proxy timeouts and buffering. If both cut off, the producer is crashing or stalling: read its logs.
What it means
Chromium’s ERR_INCOMPLETE_CHUNKED_ENCODING (-355) fires when the connection closes before the terminating zero-length chunk arrives. HTTP/1.1 chunked transfer coding sends the body as a series of chunks, each a hexadecimal byte count followed by that many bytes of data. RFC 9112 Sections 7.1 and 8 define how the stream ends and say a response that stops early must be recorded as incomplete.
Wire bodies and outputs on this page are trimmed examples. Here \r\n stands in for CRLF; the body carries the five bytes hello and ends properly:
5\r\nhello\r\n0\r\n\r\n
If the server closes after 5\r\nhello\r\n, the response is incomplete. Connection: close doesn’t stand in for the final chunk. Trailers, if any, go after the zero-size chunk and end with a blank line.
Serving that cut-off response from a loopback test server, curl 8.7.1 on macOS printed:
curl: (18) transfer closed with outstanding read data remaining
That exact wording comes from the chunk decoder in curl 8.7.1, and it’s still there in 8.22.0. Exit code 18 on its own is broader, though: it means any partial transfer, chunked or not.
The same test server made Node 26.10.0 node:http emit ECONNRESET: aborted on the response’s error event, and made Python 3.14.8 http.client raise IncompleteRead(5 bytes read) when reading the whole body. Python’s byte count depends on how much arrived. Node uses the same message for other early closes, so check the HTTP framing before you blame a missing chunk.
HTTP/2 has its own framing and forbids Transfer-Encoding entirely (RFC 9113 Section 8.2.2). The commands below force HTTP/1.1 to compare hops, which means they may take a different proxy path from the one the browser negotiated.
Find the hop that stopped sending
curl --http1.1 -v --no-buffer 'https://example.com/stream'
# On the origin host, bypass the reverse proxy:
curl --http1.1 -v --no-buffer -H 'Host: example.com' \
'http://127.0.0.1:3000/stream'
Match the failing request’s method, authorization and body. --no-buffer turns off curl’s own output buffering, but the server still decides when to flush. Watch when data last arrived and how long the line was silent before the close.
For a finite response, save the raw HTTP/1.1 chunk framing with content decoding off:
curl --http1.1 --raw -sS -D response.headers \
-H 'Accept-Encoding: identity' -o chunks.raw \
'https://example.com/finite-response'
xxd chunks.raw | tail
Confirm Transfer-Encoding: chunked, walk the chunk boundaries and look for the final zero chunk. A literal 0 inside the data isn’t a terminator. Let the transfer run to the end: piping through head or setting a client time limit truncates the response yourself, and then you’re debugging your own cut-off. An SSE stream that stays open has no final chunk until it ends normally, so use the live comparison above for streams.
Fix it, in diagnostic order
1. The upstream died after starting the body
Line up the failure time with app exceptions, worker exits, restarts and request IDs:
docker logs --since 10m my-app
docker inspect --format '{{json .State}}' my-app
sudo tail -n 100 /var/log/nginx/error.log
nginx’s upstream source logs upstream prematurely closed connection when the upstream ends the response early. Read the rest of the log line. While reading response headers means it failed before the body started. A failure mid-body is the case that hands the client headers plus partial data.
Fix whatever killed the producer: the exception, the killed process, the interrupted stream. When the response finishes normally, end it with your framework’s response-ending API so the library writes the terminator. Writing 0\r\n\r\n yourself as application data into a response the HTTP library is already chunking corrupts the body instead of ending it.
2. A silent gap exceeded a proxy or load-balancer timeout
sudo nginx -T 2>&1 | grep -nE 'proxy_read_timeout|proxy_buffering|proxy_ignore_headers'
sudo rg -n 'upstream timed out|upstream prematurely closed' /var/log/nginx/error.log
aws elbv2 describe-load-balancer-attributes --load-balancer-arn "$ALB_ARN"
Keep the nginx -T dump on the box, since it can contain deployment details. proxy_read_timeout defaults to 60 seconds between upstream reads. It’s not a cap on the whole response; it’s how long the upstream may go quiet. If that happens before headers, you get a 504. After the response has started, the client gets a truncated body instead.
The AWS Application Load Balancer idle timeout also defaults to 60 seconds, but it’s a separate timer on idle client or target connections, and HTTP/2 PING frames don’t reset it. Measure the longest silent gap and compare it with the timeout on every hop.
For SSE or an LLM token stream, have nginx pass each upstream write straight through. In this route config, 300 seconds is a chosen allowance, not a default:
location /stream {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 300s;
proxy_set_header Accept-Encoding identity;
gzip off;
}
Run nginx -t before reloading. The config turns off nginx’s compression on this route and asks the upstream for unencoded output, which the app has to honor. The load balancer’s timer is separate and stays as it was.
Or let the upstream opt out of buffering itself by sending these SSE response fields before the body:
Content-Type: text/event-stream
X-Accel-Buffering: no
nginx honors X-Accel-Buffering: no unless it’s configured to ignore it. It has to come from the upstream; adding it with a downstream add_header tells nginx’s upstream buffer nothing. And proxy_request_buffering is about the incoming request body, so it won’t fix response buffering.
During pauses in token generation, send and flush SSE comment heartbeats. The HTML Standard suggests one roughly every 15 seconds to keep legacy proxies from timing out. Pick an interval shorter than the smallest idle timeout on your path, and confirm the heartbeats actually reach the public endpoint. The heartbeat bytes are just:
: keepalive\n\n
Drive heartbeats from their own timer so they keep flowing when generation stalls. A heartbeat stuck in the app’s or compressor’s buffer keeps nothing alive downstream.
3. nginx cannot write its buffered response to disk
With proxy buffering enabled, a response too big for the memory buffers spills into proxy_temp_path. If that temp-file write fails, the event pipe aborts, and the file-write log message names the path and OS error. So a full disk can genuinely truncate responses. Look for that log line before blaming disk space for every truncation.
sudo nginx -T 2>&1 | grep -nE 'proxy_temp_path|proxy_max_temp_file_size|proxy_buffering'
sudo rg -n 'No space left on device|Permission denied|pwrite|writev' /var/log/nginx/error.log
# Replace with the actual proxy temp directory:
df -h /var/lib/nginx/proxy
df -i /var/lib/nginx/proxy
ls -ld /var/lib/nginx/proxy
Find the failing path, then check free space, free inodes and whether nginx workers can write there. Fix the capacity or permissions. On a streaming route, the proxy_buffering off config above skips this buffering path altogether, though an upstream crash still needs fixing at the upstream.
4. Compression buffered the stream or never finished
curl --http1.1 -v --no-buffer -H 'Accept-Encoding: identity' \
'https://example.com/stream'
curl --http1.1 -v --no-buffer --compressed 'https://example.com/stream'
Compare when heartbeats arrive in each run, and what Content-Encoding comes back. gzip and chunked framing work fine together. The catch is that a compressor holds data until it has enough to emit, as Node’s zlib documentation explains, so call its flush API whenever an event must reach the client now. On normal completion, finish the compressed stream before ending the HTTP response. Turning gzip off for the diagnostic route takes one buffering layer out of the picture. If the chunk framing completes but the compressed bytes are corrupt, that’s a different error: see content decoding failed.
5. An inspecting proxy buffered or interrupted delivery
Try the same request from another network you’re allowed to use, then test any explicitly configured proxy on its own:
curl --noproxy '*' --http1.1 -v --no-buffer 'https://example.com/stream'
curl --proxy http://proxy.example:8080 --http1.1 -v --no-buffer \
'https://example.com/stream'
--noproxy '*' skips curl’s configured proxy, but a transparent gateway or endpoint antivirus sits in the path regardless. As one example, Fortinet documents whole-file buffering before proxy antivirus scanning in FortiOS 7.0.5. That shows such products buffer; it doesn’t show one caused your Chrome error. If only the inspected path fails, ask the network operator to check that product’s buffering, timeout and scan logs for your request, and to make a route-specific change the product supports.
Related
Frequently asked questions
What does ERR_INCOMPLETE_CHUNKED_ENCODING mean?
The HTTP/1.1 connection closed before the client received the terminating zero-length chunk. Headers and some body data may already have arrived, so the request can display 200 OK while the body transfer fails.
Is proxy_read_timeout a limit on total response duration?
No. nginx applies it between successive reads from the upstream. The documented default is 60 seconds. An actively transmitting response can last longer; a silent gap can expire the timer.
Does proxy_buffering off prevent every streaming timeout?
No. It stops nginx from buffering the upstream response, but the application, compressor, load balancer or another proxy can still buffer or close the connection. Test heartbeats at the public endpoint.
Can gzip work with a chunked streaming response?
Yes. Compression and chunk framing are separate layers. The compressor must flush data for timely delivery and finish the compressed stream on normal completion; HTTP/1.1 must also send the terminating chunk.
Sources
- MDN: Transfer-Encodingdeveloper.mozilla.org
- RFC 9112: chunked coding and incomplete messagesrfc-editor.org
- RFC 9113: HTTP/2 connection-specific fieldsrfc-editor.org
- Chromium source: INCOMPLETE_CHUNKED_ENCODINGchromium.googlesource.com
- curl 8.7.1 source: incomplete chunked responsegithub.com
- curl 8.22.0 source: incomplete chunked responsegithub.com
- curl: --no-buffer, --http1.1 and --rawcurl.se
- nginx: proxy timeouts, buffering and temporary filesnginx.org
- nginx source: upstream termination and timeoutsgithub.com
- nginx source: failed temporary-file writes abort the pipegithub.com
- nginx source: file-write error logginggithub.com
- AWS: Application Load Balancer idle timeoutdocs.aws.amazon.com
- WHATWG HTML: SSE heartbeat commentshtml.spec.whatwg.org
- MDN: using server-sent eventsdeveloper.mozilla.org
- Node.js: premature HTTP response closurenodejs.org
- Node.js: flushing compressed outputnodejs.org
- Python: IncompleteReaddocs.python.org
- Fortinet: proxy antivirus buffering in FortiOS 7.0.5docs.fortinet.com
Related
ERR_CONTENT_DECODING_FAILED: Fix Broken Compression
Fix ERR_CONTENT_DECODING_FAILED by comparing raw bytes with Content-Encoding, checking double compression, truncated gzip and cached encoding variants.
nginx 504 Gateway Timeout: Fix Upstream Timed Out (110)
Fix nginx 504 Gateway Time-out and 'upstream timed out (110)': proxy_read_timeout, fastcgi_read_timeout, ALB idle timeout, and Cloudflare 524 compared.
Transfer-Encoding Header
Learn how the Transfer-Encoding header specifies encoding formats like chunked transfer for streaming responses when content length is unknown beforehand.
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.