gotify/server · error · errCannotParseToken
%w: timestamp must be an integer: %w
Error message
%w: timestamp must be an integer: %w
What it means
ParseEnhancedToken parses a dot-separated enhanced token of the form pubkey.pk.timestamp.signature. Field 3 (index 2) is the issuance timestamp and must parse as a base-10 int64. If strconv.ParseInt fails, the token is rejected and the error is wrapped in errCannotParseToken so callers can errors.Is/As against it.
Source
Thrown at auth/token.go:141
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 {
return nil, fmt.Errorf("%w: public key must be %d bytes", errCannotParseToken, ed25519.PublicKeySize)
}
timestampStr := fields[2]
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("%w: timestamp must be an integer: %w", errCannotParseToken, err)
}
signatureB64 := fields[3]
signatureBytesLen := base64.RawURLEncoding.DecodedLen(len(signatureB64))
if signatureBytesLen != ed25519.SignatureSize {
return nil, fmt.Errorf("%w: signature must be %d bytes", errCannotParseToken, ed25519.SignatureSize)
}
signature, err := base64.RawURLEncoding.DecodeString(signatureB64)
if err != nil {
return nil, fmt.Errorf("%w: base64 decode failed: %w", errCannotParseToken, err)
}
sha512 := sha512.New()
sha512.Write([]byte("iat=")) // query-like encoding to give us some semantic headroom should we need more fields in the future
fmt.Fprintf(sha512, "%d", timestamp)
if err := ed25519.VerifyWithOptions(pkOrPubkey, sha512.Sum(nil), signature, &ed25519.Options{Hash: crypto.SHA512}); err != nil {
return nil, errInvalidToken
}
return &EnhancedToken{
ident: ident,View on GitHub (pinned to 14bfc25627)
Solutions
- Regenerate the token on the server with the current signing code so the timestamp field is a plain base-10 int64 (unix seconds).
- Verify the token has exactly the expected number of dot-separated fields and that field index 2 is the timestamp; do not reorder fields.
- Check the token was not truncated or mangled in transit (trailing whitespace stripped, no URL-encoding of dots).
- Handle the wrapped errCannotParseToken in the caller and prompt the user to re-authenticate instead of retrying.
Example fix
// before
token := "pk.pk. 1757000000.sig" // timestamp field contains a space
parsed, err := ParseEnhancedToken(token)
// after
token := "pk.pk.1757000000.sig" // field 2 is a clean int64
parsed, err := ParseEnhancedToken(token)
if errors.Is(err, errCannotParseToken) { /* re-authenticate */ } Defensive patterns
Strategy: validation
Validate before calling
parts := strings.Split(token, ".")
if len(parts) != 4 {
return fmt.Errorf("token must have 4 dot-separated fields, got %d", len(parts))
}
if _, err := strconv.ParseInt(parts[2], 10, 64); err != nil {
return fmt.Errorf("timestamp field %q is not an int64", parts[2])
} Type guard
func isParseableToken(token string) bool {
parts := strings.Split(token, ".")
if len(parts) != 4 { return false }
_, err := strconv.ParseInt(parts[2], 10, 64)
return err == nil
} Try / catch
parsed, err := ParseEnhancedToken(token)
if errors.Is(err, errCannotParseToken) {
// token malformed: force re-auth, do not retry
return nil, fmt.Errorf("malformed token: %w", err)
}
if err != nil {
return nil, err // signature verification failed (errInvalidToken)
} Prevention
- Generate tokens only via the library's signer so field order and formats stay consistent
- Trim whitespace and reject tokens with unexpected length before parsing
- Never hand-edit token fields; treat tokens as opaque strings
- Pin the token format version and re-issue tokens when the format changes
When it happens
Trigger: Calling ParseEnhancedToken with a token whose third dot-separated field is not a valid decimal integer (empty field, non-numeric text, float like '1757000000.5', or a value overflowing int64 such as a 20+ digit number).
Common situations: Tokens truncated when copy-pasting, tokens hand-assembled or tampered with, mismatched token format after a library version change that added or reordered fields, or passing a base64/URL-encoded token where the encoding corrupted digits.
Related errors
- %w: signature must be %d bytes
- %w: private key must be %d bytes
- %w: public key must be %d bytes
- cannot delete internal application
- file with key 'file' must be present
AI-assisted analysis of gotify/server@14bfc25627 (2026-09-05).
Data as JSON: /api/errors/3aa6eea3459a96b3.
Report an issue: GitHub.