Go: unexpected EOF while reading a stream
Diagnose Go "unexpected EOF" errors from truncated HTTP bodies, short files, or connection resets, and handle partial reads correctly.
Symptoms
- unexpected EOF
- json.Decoder or io.ReadAll fails mid-stream
- The error appears only on large responses or slow connections
Possible causes
- The server or proxy closed the connection before sending Content-Length bytes
- A file was truncated during a copy or interrupted download
- The reader is read once while the writer expects multiple reads
- A TLS or proxy timeout cut the stream before completion
Diagnosis steps
Distinguish EOF from unexpected EOF in the error chain
Interpretation
io.EOF at the natural end is normal; unexpected EOF means the stream ended before the expected amount.
Log the received and expected byte counts
io.Copy with a counting writerInterpretation
A received count below Content-Length confirms truncation rather than a parse bug.
Test the same endpoint with curl and size checks
curl -sS -o out.bin -w "%{size_download}\n" URLInterpretation
Reproducing with curl separates client code from transport truncation.
Root cause
io.EOF signals a clean end of input; unexpected EOF means the reader expected more data than the stream provided. Truncated HTTP bodies (when Content-Length exceeds delivered bytes), interrupted file copies, and connections closed mid-transfer all surface this because the decoder or ReadAll believed more bytes were coming.
Fix
Make reads tolerant and verify integrity: wrap reads to detect short bodies, check Content-Length against received size, and retry idempotent requests. For files, compare sizes or checksums after copy. Use io.ReadFull when an exact length is required, and propagate context cancellation so timeouts produce a clear error rather than a truncated stream.
Verify the fix
Repeat the transfer and confirm the byte count equals Content-Length and the checksum matches; the unexpected EOF no longer occurs and partial reads are detected explicitly.
Symptoms
A read fails with unexpected EOF rather than a clean EOF. It appears on large responses, slow links, or after a proxy timeout, and small payloads are unaffected.
Diagnosis
Separate a clean end from truncation and count bytes:
n, err := io.Copy(dst, resp.Body)
log.Printf("copied=%d content-length=%d err=%v", n, resp.ContentLength, err)
A copied count below Content-Length confirms the body was cut short.
Fix
Detect short reads and retry idempotent requests:
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read body: %w", err) // surface unexpected EOF
}
if int64(len(data)) != resp.ContentLength {
return fmt.Errorf("short body: got %d want %d", len(data), resp.ContentLength)
}
Verification
Repeat the request and confirm the received length matches Content-Length and checksums agree. The unexpected EOF is gone, and any truncation now raises an explicit, actionable error.
Additional Notes
The distinction between a clean end of stream and a truncated one is central: a decoder that reaches the natural end returns a plain end-of-file, while a premature end signals that framing promised more bytes than arrived. Checking the declared length against the received count turns a vague decode failure into a precise short read, and logging both values alongside the URL or file path makes intermittent network truncation obvious. For files, comparing checksums after a copy prevents a partially written artifact from being mistaken for valid input.