{"record":{"id":"dbed42d3d9882edf","repo":"Billionmail/BillionMail","slug":"failed-to-parse-jwt-w","errorCode":null,"errorMessage":"failed to parse JWT: %w","messagePattern":"failed to parse JWT: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/batch_mail/jwt.go","lineNumber":160,"sourceCode":"\n// ParseUnsubscribeJWT 解析退订JWT\nfunc ParseUnsubscribeJWT(tokenString string) (*UnsubscribeClaims, error) {\n\tif tokenString == \"\" {\n\t\treturn nil, errors.New(\"empty token string\")\n\t}\n\n\tcfg := getConfig()\n\n\ttoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\n\t\t// Validate signing method\n\t\tif _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n\t\t\treturn nil, fmt.Errorf(\"unexpected signing method: %v\", token.Header[\"alg\"])\n\t\t}\n\t\treturn []byte(cfg.secret), nil\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse JWT: %w\", err)\n\t}\n\n\tif claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {\n\t\tresult := &UnsubscribeClaims{}\n\n\t\t// Extract email\n\t\tif email, ok := claims[\"email\"].(string); ok {\n\t\t\tresult.Email = email\n\t\t} else {\n\t\t\treturn nil, errors.New(\"JWT missing email claim\")\n\t\t}\n\n\t\t// Extract template ID\n\t\tif templateID, ok := claims[\"template_id\"].(float64); ok {\n\t\t\tresult.TemplateId = int(templateID)\n\t\t}\n\n\t\t// Extract task ID","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/batch_mail/jwt.go#L142-L178","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Verify the secret used to sign the token matches cfg.secret in getConfig() (same env/config source)","Log the underlying err (errors.Unwrap) to distinguish malformed-token vs signature-mismatch vs signing-method failures","Regenerate the unsubscribe link so a freshly signed token is issued","Check that the full token string reaches the handler (no URL-encoding/truncation loss)"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if tokenString == \"\" || strings.Count(tokenString, \".\") != 2 {\n\treturn fmt.Errorf(\"malformed unsubscribe token\")\n}","typeGuard":"func looksLikeJWT(s string) bool {\n\tparts := strings.Split(s, \".\")\n\treturn len(parts) == 3 && parts[0] != \"\" && parts[1] != \"\" && parts[2] != \"\"\n}","tryCatchPattern":"claims, err := ParseUnsubscribeJWT(tok)\nif err != nil {\n\tlog.Printf(\"unsubscribe jwt rejected: %v\", err) // includes wrapped cause\n\treturn nil, ErrInvalidUnsubscribeToken\n}","preventionTips":["Use errors.Is/As on the wrapped cause to distinguish failure kinds","Keep signing and verification secrets sourced from the same config object","Rotate secrets with a grace period; old emailed links will fail otherwise","Add integration tests covering tampered and truncated tokens"],"tags":["jwt","go","auth"],"backgroundTag":"jwt-signature-verification-failed","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}