FiloSottile/age · error
invalid closing line: %q
Error message
invalid closing line: %q
What it means
The armor reader validates that the final line of the armored stream is exactly the Footer constant ("-----END AGE ENCRYPTED FILE-----"). When the remaining short line before EOF does not match, Read returns this error, meaning the armor block is truncated or corrupted at its end.
Source
Thrown at armor/armor.go:180
}
// Reject newline characters ignored by base64.Decode.
if bytes.ContainsAny(line, "\n\r") {
return 0, r.setErr(errors.New("unexpected newline character"))
}
r.unread = r.buf[:]
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
if err != nil {
return 0, r.setErr(err)
}
r.unread = r.unread[:n]
if n < format.BytesPerLine {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) != Footer {
return 0, r.setErr(fmt.Errorf("invalid closing line: %q", line))
}
r.setErr(drainTrailing())
}
nn := copy(p, r.unread)
r.unread = r.unread[nn:]
return nn, nil
}
type Error struct {
err error
}
func (e *Error) Error() string {
return "invalid armor: " + e.err.Error()
}
func (e *Error) Unwrap() error {View on GitHub (pinned to b74dce4cdb)
Solutions
- Re-obtain or re-download the armored file — truncation during transfer is the usual cause; compare file sizes or checksums with the sender
- Inspect the end of the file and ensure the last line is exactly "-----END AGE ENCRYPTED FILE-----" with no appended content
- If extra trailing content exists (e.g. appended text), strip everything after the footer line before decrypting
- Ask the sender to re-export/re-send the file with `age -a` if the source itself is damaged
Example fix
// before
tail, _ := os.ReadFile("msg.age")
// file ends with "---END AGE ENCRYPTED FILE---" (corrupted footer)
// armor read fails: "invalid closing line"
// after
// restore/verify footer before reading
fixed := strings.TrimRight(string(tail), "\n") + "\n-----END AGE ENCRYPTED FILE-----\n"
r := armor.NewReader(strings.NewReader(fixed)) Defensive patterns
Strategy: validation
Validate before calling
func hasAgeArmorFooter(data []byte) bool {
s := string(data)
idx := strings.LastIndex(s, "-----END AGE ENCRYPTED FILE-----")
if idx < 0 { return false }
return strings.TrimSpace(s[idx:]) == "-----END AGE ENCRYPTED FILE-----"
} Try / catch
_, err := io.ReadAll(armor.NewReader(f))
if err != nil && strings.Contains(err.Error(), "invalid closing line") {
// treat file as truncated: re-fetch from sender, compare size/checksum
} Prevention
- Verify file size or checksum against the sender after any transfer
- Never append text or logs after an armored block in shared files
- Compare complete files, not fragments, when copying armor between systems
- Check for complete transfer (e.g. rsync -c) before attempting decryption
When it happens
Trigger: Calling Read on armor.Reader where the input's closing line differs from the expected footer — the armored file was truncated mid-transfer, extra text was appended or the footer line was edited, or the stream ended after a partial final data line followed by a wrong/missing footer.
Common situations: Incomplete file downloads or interrupted scp/rsync transfers cutting off the tail of the file; copy-paste of armor into editors/emails that dropped or altered the last line; tools appending logs after the armor block; diff/merge tooling corrupting the footer line.
Related errors
- invalid first line: %q
- ArmoredWriter already closed
- trailing data after armored file
- too much trailing whitespace
- too much leading whitespace
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/739883fdf6dcb3a6.
Report an issue: GitHub.