knadh/listmonk · error

error unmarshalling SNS notification: %v

Error message

error unmarshalling SNS notification: %v

What it means

SES.ProcessSubscription handles SNS SubscriptionConfirmation/UnsubscriptionConfirmation messages. It first JSON-unmarshals the raw SNS POST body into sesNotif; if the body isn't valid JSON or fields have wrong types, it returns 'error unmarshalling SNS notification' before any signature verification.

Source

Thrown at internal/bounce/webhooks/ses.go:85

// requests and bounce notifications.
type SES struct {
	mu    sync.RWMutex
	certs map[string]*x509.Certificate
}

// NewSES returns a new SES instance.
func NewSES() *SES {
	return &SES{
		certs: make(map[string]*x509.Certificate),
	}
}

// ProcessSubscription processes an SNS topic subscribe / unsubscribe notification
// by parsing and verifying the payload and calling the subscribe / unsubscribe URL.
func (s *SES) ProcessSubscription(b []byte) error {
	var n sesNotif
	if err := json.Unmarshal(b, &n); err != nil {
		return fmt.Errorf("error unmarshalling SNS notification: %v", err)
	}
	if err := s.verifyNotif(n); err != nil {
		return err
	}

	// Make an HTTP request to the sub/unsub URL.
	u := n.SubscribeURL
	if n.Type == "UnsubscriptionConfirmation" {
		u = n.UnsubscribeURL
	}

	resp, err := http.Get(u)
	if err != nil {
		return fmt.Errorf("error requesting subscription URL: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("non 200 response on subscription URL: %v", resp.StatusCode)

View on GitHub (pinned to 670c01717d)

Solutions

  1. Log the raw request body on this error and lint it as JSON
  2. Verify the SNS topic subscription points to the correct listmonk SES webhook URL with HTTP POST (SNS default content type)
  3. Re-request the topic subscription so SNS sends a fresh SubscriptionConfirmation envelope
  4. When testing manually, POST a realistic SNS envelope with Type, MessageId, TopicArn, Timestamp, SignatureVersion, Signature, SigningCertURL as strings

Example fix

// before
curl -X POST .../ses -d 'not json'
// after
curl -X POST .../ses -H 'Content-Type: text/plain' -d '{"Type":"SubscriptionConfirmation","MessageId":"id","TopicArn":"arn:aws:sns:us-east-1:1:t","Timestamp":"2024-01-01T00:00:00Z","SignatureVersion":"1","Signature":"...","SigningCertURL":"https://sns.us-east-1.amazonaws.com/SimpleNotificationService-x.pem","SubscribeURL":"https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&..."}'
Defensive patterns

Strategy: validation

Validate before calling

func validSNSEnvelope(b []byte) bool {
    var probe struct {
        Type           string `json:"Type"`
        MessageId      string `json:"MessageId"`
        TopicArn       string `json:"TopicArn"`
        Message        string `json:"Message"`
        Signature      string `json:"Signature"`
        SigningCertURL string `json:"SigningCertURL"`
        Timestamp      string `json:"Timestamp"`
    }
    return json.Unmarshal(b, &probe) == nil && probe.Type != "" && probe.SigningCertURL != ""
}
// return 400 if !validSNSEnvelope(body)

Try / catch

err := handler.ProcessSubscription(body)
if err != nil {
    if strings.HasPrefix(err.Error(), "error unmarshalling SNS notification") {
        log.Printf("SNS raw body: %s", string(body))
        http.Error(w, "invalid SNS payload", http.StatusBadRequest)
        return
    }
    http.Error(w, "webhook error", http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: The SNS subscription endpoint receives a body that fails json.Unmarshal into sesNotif: empty body, HTML error page, malformed JSON, a JSON object that isn't an SNS envelope, or non-string values for fields like Timestamp/TopicArn.

Common situations: Pointing the SNS topic subscription at the wrong URL (another route's response body); testing the endpoint with hand-written or missing JSON; a load balancer returning an error page; SNS schema changes adding unexpected types; hitting the endpoint with the wrong HTTP method or content type.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


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