FiloSottile/age · error
unexpected newline character
Error message
unexpected newline character
What it means
The armored reader splits input into lines, but base64 decoding is strict, and newline characters inside a 'line' would be silently ignored by the decoder, enabling malleability. To prevent this, Read explicitly rejects any line containing \n or \r with this error.
Source
Thrown at armor/armor.go:165
}
r.started = true
}
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
if string(line) == Footer {
return 0, r.setErr(drainTrailing())
}
if len(line) == 0 {
return 0, r.setErr(errors.New("empty line in armored data"))
}
if len(line) > format.ColumnsPerLine {
return 0, r.setErr(errors.New("column limit exceeded"))
}
// 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())
}View on GitHub (pinned to b74dce4cdb)
Solutions
- Convert the file to Unix line endings (dos2unix or sed 's/\r$//') and retry.
- Transfer the file in binary mode / byte-safe channels instead of text-normalizing protocols.
- If using a custom io.Reader feeding armor.NewReader, ensure it returns raw bytes without altering line endings.
Example fix
// before: ASCII-mode transfer adds CRs // after sed 's/\r$//' file.age > file-unix.age # or use ftp binary mode
Defensive patterns
Strategy: validation
Validate before calling
// Normalize line endings before handing data to the armor reader
func normalizeCRLF(r io.Reader) (io.Reader, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return bytes.NewReader(bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))), nil
} Type guard
func hasEmbeddedNewlines(line []byte) bool {
return bytes.ContainsAny(line, "\n\r")
} Try / catch
_, err := io.ReadAll(armor.NewReader(f))
if err != nil && err.Error() == "unexpected newline character" {
// re-read after normalizing CR/LF in the file
} Prevention
- Transfer .age files in binary mode only.
- Convert CRLF to LF (dos2unix) after text-based transfers.
- Never post-process armored bytes with line-ending converting tools.
When it happens
Trigger: Reading an armored stream where a body line still contains embedded newline or carriage-return characters — typically CR bytes from Windows CRLF files surviving line splitting, or a custom Reader passed to armor.NewReader whose Read returns lines split unusually.
Common situations: Transferring .age armored files via protocols or scripts that rewrite line endings (CRLF vs LF), FTP ASCII-mode transfers on Windows, or wrappers around the reader that rejoin/split buffers incorrectly.
Related errors
- trailing data after armored file
- too much trailing whitespace
- too much leading whitespace
- empty line in armored data
- column limit exceeded
AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31).
Data as JSON: /api/errors/11f20b47f126e6c5.
Report an issue: GitHub.