FiloSottile/age · error
failed to read chunk at offset %d: %w
Error message
failed to read chunk at offset %d: %w
What it means
DecryptReaderAt.ReadAt failed to read the ciphertext bytes for the chunk covering the requested offset from the underlying io.ReaderAt. readFullAt converted a short read into io.ErrUnexpectedEOF, or the source returned a hard error. The wrapped error identifies the true cause; decryption never ran because the ciphertext was not available.
Source
Thrown at internal/stream/stream.go:434
if len(p) == 0 {
return 0, nil
}
var cacheUpdate *cachedChunk
chunk := make([]byte, encChunkSize)
for len(p) > 0 && off < r.size {
chunkIndex := off / ChunkSize
chunkOff := chunkIndex * encChunkSize
encSize := r.size + r.chunks*chacha20poly1305.Overhead
chunkSize := min(encSize-chunkOff, encChunkSize)
cached := r.cache.Load()
var plaintext []byte
if cached != nil && cached.off == chunkOff {
plaintext = cached.data
cacheUpdate = nil
} else {
if err := readFullAt(r.src, chunk[:chunkSize], chunkOff); err != nil {
return n, fmt.Errorf("failed to read chunk at offset %d: %w", chunkOff, err)
}
nonce := nonceForChunk(chunkIndex)
if chunkIndex == r.chunks-1 {
setLastChunkFlag(nonce)
}
var err error
plaintext, err = r.a.Open(chunk[:0], nonce[:], chunk[:chunkSize], nil)
if err != nil {
return n, fmt.Errorf("failed to decrypt and authenticate chunk at offset %d: %w", chunkOff, err)
}
cacheUpdate = &cachedChunk{off: chunkOff, data: plaintext}
}
plainChunkOff := int(off - chunkIndex*ChunkSize)
copySize := min(len(plaintext)-plainChunkOff, len(p))
copy(p, plaintext[plainChunkOff:plainChunkOff+copySize])
p = p[copySize:]
off += int64(copySize)View on GitHub (pinned to b74dce4cdb)
Solutions
- Verify the source still contains all ciphertext bytes; compare current length with the size used at NewDecryptReaderAt.
- Re-fetch/re-sync the underlying data if it was truncated or partially downloaded.
- If using a custom ReaderAt, fix it to return io.EOF on end-of-data and (n, err) exactly per the interface contract.
- Retry if the underlying error is transient (the %w wrapper tells you which it was).
Defensive patterns
Strategy: try-catch
Try / catch
n, err := dr.ReadAt(p, off)
if err != nil && !errors.Is(err, io.EOF) {
var cause error
if errors.As(err, &cause) && errors.Is(cause, io.ErrUnexpectedEOF) {
return fmt.Errorf("ciphertext truncated or source shrank: %w", err)
}
// otherwise retry if transient
return err
} Prevention
- Keep the source stable and fully downloaded before random-access decryption.
- Re-validate the size against the live source if the storage can change underneath you.
- Implement custom ReaderAts strictly per the io.ReaderAt contract.
When it happens
Trigger: ReadAt(p, off) where the chunk at chunkIndex*65552 cannot be fully read: src shorter than the declared size (truncated file); a flaky ReaderAt (network file, remote block store) erroring mid-read; an incorrect custom ReaderAt implementation.
Common situations: Remote/network storage that shrank or lost data between NewDecryptReaderAt and ReadAt; sparse or partially downloaded files; disk errors; a ReaderAt wrapper that mis-reports n and err per the io.ReaderAt contract.
Related errors
- failed to read final chunk: %w
- non-EOF error reading after end of encrypted file: %w
- offset out of range [0:%d]: %d
- invalid encrypted payload size: %d
- failed to decrypt and authenticate final chunk: %w
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/623f5084c0c3e250.
Report an issue: GitHub.