Billionmail/BillionMail · warning
empty token string
Error message
empty token string
What it means
ParseUnsubscribeJWT validates and parses the signed unsubscribe JWT. Before touching the token it rejects an empty tokenString with errors.New("empty token string"). This is an input-validation guard so the JWT library never receives a blank string.
Source
Thrown at core/internal/service/batch_mail/jwt.go:146
claims := UnsubscribeClaims{
Email: email,
TemplateId: templateId,
TaskId: taskId,
GroupId: GroupId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(cfg.expiry)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(cfg.secret))
}
// ParseUnsubscribeJWT 解析退订JWT
func ParseUnsubscribeJWT(tokenString string) (*UnsubscribeClaims, error) {
if tokenString == "" {
return nil, errors.New("empty token string")
}
cfg := getConfig()
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Validate signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(cfg.secret), nil
})
if err != nil {
return nil, fmt.Errorf("failed to parse JWT: %w", err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
result := &UnsubscribeClaims{}View on GitHub (pinned to fc36c76c05)
Solutions
- Return HTTP 400 with a clear 'invalid unsubscribe link' page instead of a 500
- Regenerate the unsubscribe link/token from the contact record
- Audit email templates to ensure the token is always included in unsubscribe URLs
- Guard call sites: check the token parameter for non-empty before parsing
Example fix
// before
tokenStr := r.Get("token").String()
claims, err := ParseUnsubscribeJWT(tokenStr)
// after
tokenStr := r.Get("token").String()
if tokenStr == "" {
r.Response.WriteStatus(400, "invalid unsubscribe link: missing token")
return
}
claims, err := ParseUnsubscribeJWT(tokenStr) Defensive patterns
Strategy: validation
Validate before calling
tokenStr := r.Get("token").String()
if tokenStr == "" {
return errors.New("missing unsubscribe token")
} Type guard
func hasUnsubscribeToken(params map[string]string) bool {
return params["token"] != ""
} Try / catch
claims, err := ParseUnsubscribeJWT(tokenStr)
if err != nil {
switch {
case err.Error() == "empty token string":
// respond 400 invalid link
default:
// expired/invalid signature: respond 401 and offer resubscribe page
}
return
} Prevention
- Validate the token param exists in the handler before parsing
- Generate unsubscribe URLs with tokens always present; template-test them
- Render a friendly error page for truncated/missing tokens
- Log empty-token hits to detect email-client link truncation issues
When it happens
Trigger: Unsubscribe or UnsubscribeNew receives a request whose unsubscribe token query/form parameter is missing or blank, then calls ParseUnsubscribeJWT with "".
Common situations: Users clicking an unsubscribe link truncated by an email client; links generated by an old template before tokens were added; forwarding/copy-pasting the link without the query string; proxy stripping unknown query params.
Related errors
- supplier name, base URL, and API key are required
- JWT missing group_token claim
- Prefixes can contain only letters, numbers, underscores, and
- no recipients specified
- required column 'email' not found
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/28c13e04b10f803b.
Report an issue: GitHub.