{"record":{"id":"1128b8db2b793cb6","repo":"knadh/listmonk","slug":"error-unmarshalling-forwardemail-notification-v","errorCode":null,"errorMessage":"error unmarshalling Forwardemail notification: %v","messagePattern":"error unmarshalling Forwardemail notification: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/bounce/webhooks/forwardemail.go","lineNumber":73,"sourceCode":"\tsig, err := hex.DecodeString(sigHex)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid signature encoding: %v\", err)\n\t}\n\n\t// Generate HMAC using the request body and secret key\n\tmac := hmac.New(sha256.New, p.hmacKey)\n\tmac.Write(body)\n\texpectedSignature := mac.Sum(nil)\n\n\t// Compare the generated signature with the provided signature\n\tif !hmac.Equal(expectedSignature, sig) {\n\t\treturn nil, errors.New(\"invalid signature\")\n\t}\n\n\t// Parse the JSON payload\n\tvar n forwardemailNotif\n\tif err := json.Unmarshal(body, &n); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling Forwardemail notification: %v\", err)\n\t}\n\n\t// Categorize the bounce type\n\ttyp := models.BounceTypeSoft\n\thardBounceCategories := []string{\"block\", \"recipient\", \"virus\", \"spam\"}\n\tfor _, category := range hardBounceCategories {\n\t\tif n.Bounce.Category == category {\n\t\t\ttyp = models.BounceTypeHard\n\t\t\tbreak\n\t\t}\n\t}\n\n\tcampUUID := \"\"\n\tif v, ok := n.Headers[\"X-Listmonk-Campaign\"]; ok {\n\t\tcampUUID = v\n\t}\n\n\treturn []models.Bounce{{","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/bounce/webhooks/forwardemail.go#L55-L91","documentation":"After the HMAC signature is verified, Forwardemail's ProcessBounce unmarshals the request body into the forwardemailNotif struct. This error is thrown when the (signature-valid) body is not valid JSON or field types don't match the struct — e.g. `bounced_at` not an RFC3339 time or `bounce` not an object.","triggerScenarios":"Calling ProcessBounce with: an empty body, non-JSON bodies (form-encoded, plain text), JSON that doesn't match the schema (e.g. `bounced_at` as a unix number instead of an RFC3339 string, `bounce` as a string), or bodies signed but produced by a different provider version.","commonSituations":"Testing with a signed but hand-crafted payload that has wrong field types; Forwardemail changing their webhook schema; a middleware mutating the body after signing (re-compaction usually fine, but trimming can break JSON); posting the raw email instead of the JSON webhook envelope.","solutions":["Validate the body is valid JSON (`json.Valid(body)`) before sending/expecting success, and log the Unmarshal error to find the offending field.","Ensure `bounced_at` is an RFC3339 timestamp string and `bounce` is an object with the expected fields.","Use Forwardemail's documented bounce webhook payload as a fixture in tests.","Check no middleware is altering the request body between signature verification and parsing."],"exampleFix":"// before: wrong field types\nbody := []byte(`{\"recipient\":\"a@b.com\",\"bounced_at\":1725000000}`) // bounced_at as unix int\n\n// after: RFC3339 timestamp\nbody := []byte(`{\"recipient\":\"a@b.com\",\"bounced_at\":\"2024-08-30T10:00:00Z\"}`)","handlingStrategy":"validation","validationCode":"func isForwardemailPayload(body []byte) bool {\n    if !json.Valid(body) {\n        return false\n    }\n    var probe struct {\n        Recipient string `json:\"recipient\"`\n        BouncedAt string `json:\"bounced_at\"`\n        Bounce    struct {\n            Category string `json:\"category\"`\n        } `json:\"bounce\"`\n    }\n    if json.Unmarshal(body, &probe) != nil {\n        return false\n    }\n    _, err := time.Parse(time.RFC3339, probe.BouncedAt)\n    return probe.Recipient != \"\" && err == nil\n}","typeGuard":null,"tryCatchPattern":"bounces, err := fw.ProcessBounce(sig, body)\nif err != nil {\n    if strings.Contains(err.Error(), \"unmarshalling Forwardemail notification\") {\n        log.Printf(\"forwardemail payload schema mismatch: %v; body=%s\", err, body)\n        http.Error(w, \"invalid payload\", http.StatusBadRequest)\n        return\n    }\n    http.Error(w, \"processing failed\", http.StatusInternalServerError)\n}","preventionTips":["Keep a signed real Forwardemail bounce payload as a fixture in integration tests.","Watch Forwardemail changelogs for webhook schema changes; validate field types like bounced_at (RFC3339).","Validate json.Valid(body) before processing to fail fast with a clear client error."],"tags":["webhook","json","forwardemail","schema","bounce"],"backgroundTag":"webhook-payload-schema-mismatch","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}