knadh/listmonk · error

non 200 response on subscription URL

Error message

non 200 response on subscription URL: %v

What it means

After successfully GETting the SNS SubscribeURL/UnsubscribeURL, ProcessSubscription requires HTTP 200. Any other status code returns 'non 200 response on subscription URL', meaning SNS's confirmation endpoint responded but did not accept the confirmation.

Solutions

  1. Check the logged status code: 403/410 typically means the SubscribeURL expired or token is invalid
  2. Re-trigger the subscription from the AWS SNS console so a fresh SubscriptionConfirmation with a new URL is sent
  3. Confirm the notification being processed is a live SNS message, not a replayed/stored sample
  4. If a proxy returns the non-200, bypass it for *.amazonaws.com

Example fix

// before: replaying an old saved SNS test payload (expired URL) -> 403
// after: request a new confirmation
aws sns subscribe --topic-arn arn:aws:sns:us-east-1:1:t --protocol https --notification-endpoint https://your-host/webhook/ses
# then let the fresh SubscriptionConfirmation hit the endpoint
Defensive patterns

Strategy: retry

Validate before calling

u := n.SubscribeURL // only pass SNS-hosted URLs
parsed, err := url.Parse(u)
valid := err == nil && strings.HasSuffix(parsed.Hostname(), ".amazonaws.com")
// ensure the URL is a fresh, valid SNS confirmation URL before GET

Try / catch

err := handler.ProcessSubscription(body)
if err != nil {
    var code string
    if _, e := fmt.Sscanf(err.Error(), "non 200 response on subscription URL: %s", &code); e == nil {
        log.Printf("SNS confirmation rejected (%s); request a fresh subscription", code)
        // 403/410 => expired or replayed URL: re-subscribe via AWS CLI/console
        http.Error(w, "subscription not confirmed", http.StatusBadGateway)
        return
    }
    http.Error(w, "webhook error", http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: http.Get succeeded but the confirmation URL returned a non-200 status: expired SubscribeURL (SNS confirms are short-lived), already-confirmed subscription, replayed/old confirmation message, or SNS-side throttling/errors.

Common situations: Replaying an old stored SubscriptionConfirmation payload whose token/URL has expired; double-confirming the same subscription; network middleware (corporate proxy) intercepting and returning 403/407; SNS regional endpoint issues.

Related errors


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

Appendix: source

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

		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)
	}

	return nil
}

// ProcessBounce processes an SES bounce notification and returns a Bounce object.
func (s *SES) ProcessBounce(b []byte) (models.Bounce, error) {
	var (
		bounce models.Bounce
		n      sesNotif
	)
	if err := json.Unmarshal(b, &n); err != nil {
		return bounce, fmt.Errorf("error unmarshalling SES notification: %v", err)
	}
	if err := s.verifyNotif(n); err != nil {
		return bounce, err
	}

View on GitHub (pinned to 670c01717d)