gotify/server · error · errCannotParseToken
%w: token must have 2 or 4 fields separated by dots
Error message
%w: token must have 2 or 4 fields separated by dots
What it means
After stripping the "gtfy" prefix, ParseEnhancedToken splits the remainder on dots expecting either 2 fields (ident + private key) or 4 fields (ident + public key + challenge + signature). Any other dot count returns errCannotParseToken wrapped with "token must have 2 or 4 fields separated by dots".
Source
Thrown at auth/token.go:117
var seed [ed25519.SeedSize]byte
_, err := rand.Read(seed[:])
if err != nil {
panic("unreachable: random source should never return an error")
}
return &EnhancedToken{ident: ident, pubOrPrivKey: seed[:]}
}
// ParseEnhancedToken parses a string into an EnhancedToken.
func ParseEnhancedToken(token string) (*EnhancedToken, error) {
token, found := strings.CutPrefix(token, enhancedTokenPrefix)
if !found {
return nil, fmt.Errorf("%w: token must start with %s", errCannotParseToken, enhancedTokenPrefix)
}
// count number of dots, one dot -> ident then private key, three dots -> ident, public key, challenge then signature
fields := strings.SplitN(token, ".", 4)
if len(fields) != 2 && len(fields) != 4 {
return nil, fmt.Errorf("%w: token must have 2 or 4 fields separated by dots", errCannotParseToken)
}
ident := fields[0]
pkOrPubkeyB64 := fields[1]
pkOrPubkeyBytesLen := base64.RawURLEncoding.DecodedLen(len(pkOrPubkeyB64))
pkOrPubkey, err := base64.RawURLEncoding.DecodeString(pkOrPubkeyB64)
if err != nil {
return nil, fmt.Errorf("%w: base64 decode failed: %w", errCannotParseToken, err)
}
if len(fields) == 2 {
if pkOrPubkeyBytesLen != ed25519.SeedSize {
return nil, fmt.Errorf("%w: private key must be %d bytes", errCannotParseToken, ed25519.SeedSize)
}
return &EnhancedToken{
ident: ident,
pubOrPrivKey: pkOrPubkey,
}, nil
}
if pkOrPubkeyBytesLen != ed25519.PublicKeySize {View on GitHub (pinned to 14bfc25627)
Solutions
- Use a token produced by EnhancedToken.String()/PublicForm() rather than hand-built strings.
- Verify the token body has exactly one dot (private-key form) or three dots (public-key/challenge/signature form).
- If you hold a JWT, parse it with the JWT verifier instead of ParseEnhancedToken.
- Regenerate the token and copy it in full, unmodified.
Example fix
// before: jwt-like input
auth.ParseEnhancedToken("gtfya.b.c.d.e")
// after: correct 4-field form
auth.ParseEnhancedToken("gtfy<ident>.<pubkeyB64>.<challenge>.<signature>") Defensive patterns
Strategy: validation
Validate before calling
body := strings.TrimPrefix(tok, "gtfy")
n := strings.Count(body, ".")
if n != 1 && n != 3 {
return errors.New("enhanced token must have 2 or 4 dot-separated fields")
} Type guard
func wellFormedEnhancedToken(s string) bool {
if !strings.HasPrefix(s, "gtfy") { return false }
n := strings.Count(strings.TrimPrefix(s, "gtfy"), ".")
return n == 1 || n == 3
} Try / catch
if _, err := auth.ParseEnhancedToken(raw); errors.Is(err, auth.errCannotParseToken) {
return fmt.Errorf("malformed token: %w", err)
} Prevention
- Use EnhancedToken.String()/PublicForm() to serialize tokens; never hand-build them.
- Don't pass JWTs or other bearer formats to ParseEnhancedToken.
- Copy tokens in full — dropped segments change the dot count.
When it happens
Trigger: Calling ParseEnhancedToken with a prefixed string whose body has 1, 3, or 5+ dot-separated segments — e.g. a JWT (5 segments), a malformed manual concatenation, or a partially truncated token.
Common situations: Passing a JWT or other bearer token by mistake; manual token construction with wrong segment count; token corrupted by a newline/URL rewrite stripping a segment.
Related errors
- %w: token must start with %s
- cannot parse token
- %w: base64 decode failed: %w
- %w: private key must be %d bytes
- %w: public key must be %d bytes
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/68e45ea089d45d7b.
Report an issue: GitHub.