knadh/listmonk · error

error unmarshalling azure notification array: %v

Error message

error unmarshalling azure notification array: %v

What it means

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.

Source

Thrown at internal/bounce/webhooks/azure.go:183

	headerSecret := strings.TrimSpace(req.Header.Get(headerName))
	if secretsEqual(a.sharedSecret, headerSecret) {
		return nil
	}

	return errors.New("invalid azure event grid shared secret")
}

func secretsEqual(expected, given string) bool {
	if expected == "" || given == "" {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(expected), []byte(given)) == 1
}

func parseAzureEvents(b []byte) ([]azureEvent, error) {
	var raws []json.RawMessage
	if err := json.Unmarshal(b, &raws); err != nil {
		return nil, fmt.Errorf("error unmarshalling azure notification array: %v", err)
	}

	events := make([]azureEvent, 0, len(raws))
	for _, raw := range raws {
		ev := azureEvent{RawData: raw}
		if err := json.Unmarshal(raw, &ev); err != nil {
			return nil, fmt.Errorf("error unmarshalling azure event: %v", err)
		}
		events = append(events, ev)
	}

	return events, nil
}

func mapAzureStatus(status, details string) (string, bool) {
	s := strings.ToLower(strings.TrimSpace(status))
	d := strings.ToLower(strings.TrimSpace(details))

View on GitHub (pinned to 670c01717d)

Solutions

  1. Wrap the payload in a JSON array before sending: Event Grid expects `[{...}, {...}]`.
  2. Verify the webhook route only accepts POST with Content-Type application/json and log the raw body on failure.
  3. Check that any middleware (gzip decompression, body logging, re-signing) is not consuming or corrupting req.Body before it reaches the handler.
  4. Test the endpoint with a captured real Event Grid payload to confirm parsing works.

Example fix

// before: single object body
body := []byte(`{"eventType":"Microsoft.EventGrid.SubscriptionValidationEvent","data":{"validationCode":"x"}}`)

// after: array body as Event Grid sends
body := []byte(`[{"eventType":"Microsoft.EventGrid.SubscriptionValidationEvent","data":{"validationCode":"x"}}]`)
Defensive patterns

Strategy: validation

Validate before calling

func isJSONEventArray(body []byte) bool {
    var raws []json.RawMessage
    return len(body) > 0 && json.Unmarshal(body, &raws) == nil && len(raws) > 0
}

Type guard

func isArrayBody(body []byte) bool {
    var arr []any
    return json.Unmarshal(body, &arr) == nil
}

Try / catch

bounces, err := azure.ProcessBounce(req, body)
if err != nil {
    if strings.Contains(err.Error(), "unmarshalling azure notification array") {
        log.Printf("malformed Event Grid body: %v; raw=%q", err, string(body))
        http.Error(w, "malformed payload", http.StatusBadRequest)
        return
    }
    http.Error(w, "webhook processing failed", http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/61495ee83385cfb4. Report an issue: GitHub.