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

  1. Verify the source still contains all ciphertext bytes; compare current length with the size used at NewDecryptReaderAt.
  2. Re-fetch/re-sync the underlying data if it was truncated or partially downloaded.
  3. If using a custom ReaderAt, fix it to return io.EOF on end-of-data and (n, err) exactly per the interface contract.
  4. 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

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


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/623f5084c0c3e250. Report an issue: GitHub.