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

  1. Convert the file to Unix line endings (dos2unix or sed 's/\r$//') and retry.
  2. Transfer the file in binary mode / byte-safe channels instead of text-normalizing protocols.
  3. 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

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


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