FiloSottile/age · error

invalid character human-readable part: s[%d]=%d

Error message

invalid character human-readable part: s[%d]=%d

What it means

After splitting on the last '1', Decode validates every rune of the human-readable part is in the printable ASCII range 33-126. A rune outside that range (spaces are ok at 32? no - 32 is excluded, so space fails; control chars, and non-ASCII unicode fail) makes the HRP invalid. The library enforces the bech32 spec's printable-ASCII HRP requirement.

Source

Thrown at internal/bech32/bech32.go:154

	if lower {
		return ret.String(), nil
	}
	return strings.ToUpper(ret.String()), nil
}

// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.
func Decode(s string) (hrp string, data []byte, err error) {
	if strings.ToLower(s) != s && strings.ToUpper(s) != s {
		return "", nil, fmt.Errorf("mixed case")
	}
	pos := strings.LastIndex(s, "1")
	if pos < 1 || pos+7 > len(s) {
		return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s))
	}
	hrp = s[:pos]
	for p, c := range hrp {
		if c < 33 || c > 126 {
			return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c)
		}
	}
	for p, c := range s[pos+1:] {
		// Fold ASCII explicitly. Unicode case folding can turn a non-ASCII
		// rune into a shorter valid charset member.
		if c >= 'A' && c <= 'Z' {
			c += 'a' - 'A'
		}
		d := strings.IndexRune(charset, c)
		if d == -1 {
			return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c)
		}
		data = append(data, byte(d))
	}
	if len(data) < 6 {
		return "", nil, fmt.Errorf("data part too short")
	}
	if !verifyChecksum(hrp, data) {

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Trim and clean the input of whitespace and control characters before decoding
  2. Re-copy the key as plain ASCII from the original source
  3. Strip BOM/zero-width characters from config-sourced values

Example fix

// before
callersBech32.Decode(strings.TrimSpace(cfg.Recipient)) // may still contain unicode
// after
hrp1qq := cfg.Recipient
hrp1qq = strings.TrimSpace(hrp1qq)
hrp1qq = strings.Map(func(r rune) rune { if r >= 33 && r <= 126 { return r }; return -1 }, hrp1qq)
Defensive patterns

Strategy: validation

Validate before calling

func printableASCII(s string) bool {
    for _, r := range s {
        if r < 33 || r > 126 { return false }
    }
    return true
}
if !printableASCII(input) { return errors.New("input contains non-printable or non-ASCII characters") }

Prevention

When it happens

Trigger: Decode with a string whose text before the last '1' contains a space, control character, or any byte/rune outside 33-126, e.g. "my key1qqqqqq" or "café1qqqq".

Common situations: Keys pasted from documents with smart quotes or trailing whitespace, config files with BOM or invisible control characters, terminal copy including ANSI codes.

Understand the failure class

Related errors


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