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

  1. Return HTTP 400 with a clear 'invalid unsubscribe link' page instead of a 500
  2. Regenerate the unsubscribe link/token from the contact record
  3. Audit email templates to ensure the token is always included in unsubscribe URLs
  4. 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

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


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