{"record":{"id":"23f9e9815cca243c","repo":"FiloSottile/age","slug":"parsing-age-header","errorCode":null,"errorMessage":"parsing age header: ","messagePattern":"parsing age header: ","errorType":"exception","errorClass":"ParseError","httpStatus":null,"severity":"error","filePath":"internal/format/format.go","lineNumber":279,"sourceCode":"}\n\nfunc (r *headerReader) Peek(n int) ([]byte, error) {\n\tif r.n+n > maxHeaderBytes {\n\t\treturn nil, errorf(\"header exceeds 2 MiB\")\n\t}\n\treturn r.r.Peek(n)\n}\n\nfunc (e *ParseError) Error() string {\n\treturn \"parsing age header: \" + e.err.Error()\n}\n\nfunc (e *ParseError) Unwrap() error {\n\treturn e.err\n}\n\nfunc errorf(format string, a ...any) error {\n\treturn &ParseError{fmt.Errorf(format, a...)}\n}\n\n// describeIntro returns a quoted description of a bad intro line, or an empty\n// string if the line contains recognizable private key material.\nfunc describeIntro(line string) string {\n\tfor _, prefix := range []string{\"AGE-SECRET-KEY-\", \"AGE-PLUGIN-\"} {\n\t\tif strings.HasPrefix(line, prefix) {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\t// Preserve enough context to diagnose a mangled intro without echoing an\n\t// arbitrarily long first line.\n\treturn fmt.Sprintf(\"%q\", line[:min(len(line), len(intro))])\n}\n\n// Parse returns the header and a Reader that begins at the start of the\n// payload.\nfunc Parse(input io.Reader) (*Header, io.Reader, error) {","sourceCodeStart":261,"sourceCodeEnd":297,"githubUrl":"https://github.com/FiloSottile/age/blob/b74dce4cdbe35b5e5f66c06d9612b72f89028758/internal/format/format.go#L261-L297","documentation":"errorf constructs a *age.ParseError wrapping header-parsing failures. Callers of format.Parse prepend 'parsing age header: ', so any structural header problem (bad intro line, malformed args, bad closing line) surfaces under this prefix. Check with errors.As against *age.ParseError.","triggerScenarios":"format.Parse encounters a header that violates the format — wrong intro line ('age-encryption.org/v1' missing), invalid argument counts, malformed closing line — and wraps it via errorf before callers add the 'parsing age header: ' prefix.","commonSituations":"Pointing age at a plaintext or non-age file; decrypting armored files without armor mode; files produced by incompatible tools; corruption in the first bytes of the file.","solutions":["Confirm the input actually is an age file (starts with 'age-encryption.org/v1')","If the file is PEM-armored, wrap the reader with armor.NewReader before parsing","Re-transfer the file if bytes at the start were altered or corrupted","Use errors.As(err, *age.ParseError) to get structured detail about which line failed"],"exampleFix":"// before\nout, err := age.Decrypt(f, ids) // parsing age header: ...\n// after\nbr := bufio.NewReader(f)\nhead, _ := br.Peek(4)\nvar r io.Reader = br\nif bytes.Equal(head, []byte(\"-----\")) { r = armor.NewReader(br) }\nout, err := age.Decrypt(r, ids)","handlingStrategy":"try-catch","validationCode":"head, _ := bufio.NewReader(f).Peek(len(\"age-encryption.org/v1\\n\"))\nif !bytes.HasPrefix(head, []byte(\"age-encryption.org/v1\")) { return errors.New(\"not an age file\") }","typeGuard":"var pe *age.ParseError\nif errors.As(err, &pe) { /* structured header parse failure */ }","tryCatchPattern":"out, err := age.Decrypt(r, ids)\nif err != nil {\n    var pe *age.ParseError\n    if errors.As(err, &pe) { return fmt.Errorf(\"invalid age header: %w\", pe.Unwrap()) }\n    return err\n}","preventionTips":["Peek the intro line before parsing to reject non-age input","Decode armored files with armor.NewReader before Decrypt","Keep the decrypting tool version compatible with the encrypting one"],"tags":["format","header-parsing","parse-error"],"backgroundTag":"schema-validation-failed","analyzedSha":"b74dce4cdbe35b5e5f66c06d9612b72f89028758","analyzedAt":"2026-08-31T23:59:31.627Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}