knadh/listmonk · error

non 200 response on subscription URL: %v

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.

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)

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.