gotify/server · error · errCannotParseToken
%w: token must start with %s
Error message
%w: token must start with %s
What it means
ParseEnhancedToken only accepts tokens prefixed with the enhanced-token prefix "gtfy". If the string lacks that prefix it returns an error wrapping errCannotParseToken with "token must start with gtfy". This guards against passing plain IDs, other token formats, or truncated tokens into the enhanced-token parser.
Source
Thrown at auth/token.go:111
return b.String()
}
// NewEnhancedToken creates a new EnhancedToken.
func NewEnhancedToken(ident string) *EnhancedToken {
ident = strings.ReplaceAll(ident, ".", "_")
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)
}View on GitHub (pinned to 14bfc25627)
Solutions
- Pass the full token including the "gtfy" prefix exactly as returned by PublicForm/String().
- Check where the token is stored/generated — do not trim or transform it before parsing.
- If you have a non-enhanced token, use the appropriate parser for that format instead.
- Regenerate the token via the API so you receive a properly prefixed enhanced token.
Example fix
// before
tok, err := auth.ParseEnhancedToken("abc.def")
// after
tok, err := auth.ParseEnhancedToken("gtfyabc.def") Defensive patterns
Strategy: validation
Validate before calling
func hasEnhancedTokenPrefix(tok string) bool {
return strings.HasPrefix(tok, "gtfy")
} Type guard
func isEnhancedToken(s string) bool {
return strings.HasPrefix(s, "gtfy") && strings.Count(strings.TrimPrefix(s, "gtfy"), ".")%2 == 1
} Try / catch
tok, err := auth.ParseEnhancedToken(raw)
if errors.Is(err, auth.errCannotParseToken) {
// (exported via wrapping) treat as malformed token: re-authenticate or regenerate
} Prevention
- Store and pass tokens exactly as issued, including the gtfy prefix.
- Never trim, URL-encode, or wrap tokens in transport/storage.
- Distinguish enhanced tokens from other token types at the call site.
When it happens
Trigger: Calling ParseEnhancedToken (directly or via client/application APIs such as CreateClient or CreateApplication that accept a token parameter) with a string not beginning with "gtfy" — e.g. a raw OAuth token, a copied identifier, or the token stripped of its prefix.
Common situations: Copy-pasting only the body after the prefix; storing tokens without the prefix in env vars; confusing enhanced tokens with plain access tokens from an older version.
Related errors
- %w: token must have 2 or 4 fields separated by dots
- 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/d48d20b953d25d00.
Report an issue: GitHub.