{"record":{"id":"61495ee83385cfb4","repo":"knadh/listmonk","slug":"error-unmarshalling-azure-notification-array-v","errorCode":null,"errorMessage":"error unmarshalling azure notification array: %v","messagePattern":"error unmarshalling azure notification array: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/bounce/webhooks/azure.go","lineNumber":183,"sourceCode":"\theaderSecret := strings.TrimSpace(req.Header.Get(headerName))\n\tif secretsEqual(a.sharedSecret, headerSecret) {\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"invalid azure event grid shared secret\")\n}\n\nfunc secretsEqual(expected, given string) bool {\n\tif expected == \"\" || given == \"\" {\n\t\treturn false\n\t}\n\treturn subtle.ConstantTimeCompare([]byte(expected), []byte(given)) == 1\n}\n\nfunc parseAzureEvents(b []byte) ([]azureEvent, error) {\n\tvar raws []json.RawMessage\n\tif err := json.Unmarshal(b, &raws); err != nil {\n\t\treturn nil, fmt.Errorf(\"error unmarshalling azure notification array: %v\", err)\n\t}\n\n\tevents := make([]azureEvent, 0, len(raws))\n\tfor _, raw := range raws {\n\t\tev := azureEvent{RawData: raw}\n\t\tif err := json.Unmarshal(raw, &ev); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error unmarshalling azure event: %v\", err)\n\t\t}\n\t\tevents = append(events, ev)\n\t}\n\n\treturn events, nil\n}\n\nfunc mapAzureStatus(status, details string) (string, bool) {\n\ts := strings.ToLower(strings.TrimSpace(status))\n\td := strings.ToLower(strings.TrimSpace(details))\n","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/bounce/webhooks/azure.go#L165-L201","documentation":"parseAzureEvents first unmarshals the entire request body as a JSON array of raw events. This error is thrown when the body is not a JSON array at all — Event Grid always posts an array, so a non-array body indicates malformed or unexpected input to either ProcessSubscription or ProcessBounce.","triggerScenarios":"Calling ProcessSubscription or ProcessBounce with: an empty body, a single JSON object not wrapped in an array (e.g. `{\"eventType\":...}` instead of `[{...}]`), truncated/invalid JSON, HTML from a misrouted request, or form-encoded data.","commonSituations":"Testing the webhook with curl and forgetting the surrounding brackets; reverse proxy stripping or corrupting the body; posting a single event object from a local Azure emulator or hand-rolled test; wrong HTTP method/route hitting the handler (e.g. GET health check body); charset/gzip issues leaving the body unreadable.","solutions":["Wrap the payload in a JSON array before sending: Event Grid expects `[{...}, {...}]`.","Verify the webhook route only accepts POST with Content-Type application/json and log the raw body on failure.","Check that any middleware (gzip decompression, body logging, re-signing) is not consuming or corrupting req.Body before it reaches the handler.","Test the endpoint with a captured real Event Grid payload to confirm parsing works."],"exampleFix":"// before: single object body\nbody := []byte(`{\"eventType\":\"Microsoft.EventGrid.SubscriptionValidationEvent\",\"data\":{\"validationCode\":\"x\"}}`)\n\n// after: array body as Event Grid sends\nbody := []byte(`[{\"eventType\":\"Microsoft.EventGrid.SubscriptionValidationEvent\",\"data\":{\"validationCode\":\"x\"}}]`)","handlingStrategy":"validation","validationCode":"func isJSONEventArray(body []byte) bool {\n    var raws []json.RawMessage\n    return len(body) > 0 && json.Unmarshal(body, &raws) == nil && len(raws) > 0\n}","typeGuard":"func isArrayBody(body []byte) bool {\n    var arr []any\n    return json.Unmarshal(body, &arr) == nil\n}","tryCatchPattern":"bounces, err := azure.ProcessBounce(req, body)\nif err != nil {\n    if strings.Contains(err.Error(), \"unmarshalling azure notification array\") {\n        log.Printf(\"malformed Event Grid body: %v; raw=%q\", err, string(body))\n        http.Error(w, \"malformed payload\", http.StatusBadRequest)\n        return\n    }\n    http.Error(w, \"webhook processing failed\", http.StatusInternalServerError)\n}","preventionTips":["Reject non-POST or non-application/json requests at the route level before reading the body.","Log the raw body whenever webhook parsing fails so you can diff against a real Event Grid envelope.","Add an integration test using an actual captured Event Grid delivery payload.","Ensure gzip/charset middleware is not corrupting the request body."],"tags":["azure","webhook","json","event-grid","malformed-payload"],"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"}