FiloSottile/age · error

failed to read header: %w

Error message

failed to read header: %w

What it means

format.Parse reads the header via a bufio reader; if ReadBytes fails while reading the closing '---' footer line (EOF before newline or an I/O error), it returns this wrapped error. It means the file ended before the header's closing line was seen.

Source

Thrown at internal/format/format.go:339

		}
		return nil, nil, errorf("unexpected intro, expected %q", intro)
	}

	sr := &StanzaReader{r: hr}
	for {
		peek, err := hr.Peek(len(footerPrefix))
		if err != nil {
			// headerReader errors are already ParseErrors; don't nest the prefix.
			if _, ok := err.(*ParseError); ok {
				return nil, nil, err
			}
			return nil, nil, errorf("failed to read header: %w", err)
		}

		if bytes.Equal(peek, footerPrefix) {
			line, err := hr.ReadBytes('\n')
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read header: %w", err)
			}

			prefix, args := splitArgs(line)
			if prefix != string(footerPrefix) || len(args) != 1 {
				return nil, nil, errorf("malformed closing line: %q", line)
			}
			h.MAC, err = DecodeString(args[0])
			if err != nil || len(h.MAC) != 32 {
				return nil, nil, errorf("malformed closing line %q: %v", line, err)
			}
			break
		}
		if len(h.Recipients) == maxRecipientStanzas {
			return nil, nil, errorf("header contains more than %d recipient stanzas", maxRecipientStanzas)
		}

		s, err := sr.ReadStanza()
		if err != nil {

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Re-obtain a complete copy of the encrypted file and verify its size/hash
  2. Ensure the producer finished flushing/closing before the consumer parses
  3. Distinguish truncation from transport failure with errors.Is(err, io.ErrUnexpectedEOF) / io.EOF
  4. If the file is legitimately tiny, check it was actually written by age and not an empty/garbage file

Example fix

// before
hdr, _, err := format.Parse(f) // failed to read header: unexpected EOF
// after
st, _ := f.Stat()
if st.Size() < 100 { return fmt.Errorf("refusing parse: file is %d bytes", st.Size()) }
hdr, _, err := format.Parse(f)
Defensive patterns

Strategy: try-catch

Validate before calling

st, err := os.Stat(path)
if err != nil { return err }
if st.Size() < 100 { return fmt.Errorf("file too small to contain a full age header") }

Type guard

var pe *age.ParseError
if errors.As(err, &pe) && errors.Is(pe.Unwrap(), io.ErrUnexpectedEOF) { /* header cut before closing line */ }

Try / catch

hdr, body, err := format.Parse(r)
if err != nil {
    if errors.Is(err, io.ErrUnexpectedEOF) { return fmt.Errorf("file truncated in header: %w", err) }
    return err
}

Prevention

When it happens

Trigger: format.Parse peeks for footerPrefix, calls hr.ReadBytes('\n') for the closing line, and gets EOF or a read error — header truncated between the last stanza and the '---' MAC line.

Common situations: Files cut off mid-header by interrupted uploads/downloads; tiny garbage files; reading from a socket or pipe that closed early.

Related errors


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