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

  1. Re-obtain or re-download the armored file — truncation during transfer is the usual cause; compare file sizes or checksums with the sender
  2. Inspect the end of the file and ensure the last line is exactly "-----END AGE ENCRYPTED FILE-----" with no appended content
  3. If extra trailing content exists (e.g. appended text), strip everything after the footer line before decrypting
  4. 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

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


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