← Debugging Guides

阅读中文版本

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.

intermediatedebugginggoiohttp

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 writer

Interpretation

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" URL

Interpretation

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.