Billionmail/BillionMail · error

failed to parse JWT: %w

Error message

failed to parse JWT: %w

What it means

Generic wrap of any error returned by jwt.Parse: malformed token, bad signature, malformed claims, or the signing-method rejection from the keyfunc. ParseUnsubscribeJWT wraps the library error with %w so errors.Is/As still work on the underlying golang-jwt cause.

Source

Thrown at core/internal/service/batch_mail/jwt.go:160

// 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{}

		// Extract email
		if email, ok := claims["email"].(string); ok {
			result.Email = email
		} else {
			return nil, errors.New("JWT missing email claim")
		}

		// Extract template ID
		if templateID, ok := claims["template_id"].(float64); ok {
			result.TemplateId = int(templateID)
		}

		// Extract task ID

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Verify the secret used to sign the token matches cfg.secret in getConfig() (same env/config source)
  2. Log the underlying err (errors.Unwrap) to distinguish malformed-token vs signature-mismatch vs signing-method failures
  3. Regenerate the unsubscribe link so a freshly signed token is issued
  4. Check that the full token string reaches the handler (no URL-encoding/truncation loss)
Defensive patterns

Strategy: try-catch

Validate before calling

if tokenString == "" || strings.Count(tokenString, ".") != 2 {
	return fmt.Errorf("malformed unsubscribe token")
}

Type guard

func looksLikeJWT(s string) bool {
	parts := strings.Split(s, ".")
	return len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != ""
}

Try / catch

claims, err := ParseUnsubscribeJWT(tok)
if err != nil {
	log.Printf("unsubscribe jwt rejected: %v", err) // includes wrapped cause
	return nil, ErrInvalidUnsubscribeToken
}

Prevention

When it happens

Trigger: ParseUnsubscribeJWT receives a token string that fails jwt.Parse: corrupt/missing segments, signature computed with a different secret, expired-per-parser tokens, or the unexpected-signing-method error (240).

Common situations: Secret rotated in config but old links still in emails; token truncated by mail clients or URL handling; different environment (dev vs prod) secrets; typos when manually copying tokens.

Understand the failure class

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/dbed42d3d9882edf. Report an issue: GitHub.