Log redacted email provider errors and diagnose sender configuration (#57)

This commit is contained in:
Stephan D committed 2026-10-17 11:39:05 +02:00
1 parent c1d28e705e
commit 3e4e496037
8 files changed
+327 -23

No files matched your search

+17 -8
View File
@@ -28,14 +28,15 @@ func (c *client) post(ctx context.Context, key string, payload any) Outcome {
}
resp, err := c.http.Do(req)
if err != nil {
return Outcome{State: model.Unknown, Code: "provider_outcome_unknown"}
return Outcome{State: model.Unknown, Code: "provider_outcome_unknown", ProviderMessage: safeErrorText(err.Error(), []string{c.key, key})}
}
defer resp.Body.Close()
// Discard a bounded response without logging provider-controlled content.
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64<<10))
// Never pass raw provider bodies through the outcome or log them directly.
details, readErr := io.ReadAll(io.LimitReader(resp.Body, maxErrorBody+1))
outcome := Outcome{HTTPStatus: resp.StatusCode}
switch {
case resp.StatusCode >= 200 && resp.StatusCode < 300:
return Outcome{State: model.Accepted}
outcome.State = model.Accepted
case resp.StatusCode == 429:
delay := time.Minute
if n, e := strconv.Atoi(resp.Header.Get("Retry-After")); e == nil && n > 0 && n <= 86400 {
@@ -43,12 +44,20 @@ func (c *client) post(ctx context.Context, key string, payload any) Outcome {
} else if date, e := http.ParseTime(resp.Header.Get("Retry-After")); e == nil && time.Until(date) > 0 {
delay = time.Until(date)
}
return Outcome{State: model.RetryWait, Code: "provider_rate_limited", RetryAfter: delay}
outcome.State, outcome.Code, outcome.RetryAfter = model.RetryWait, "provider_rate_limited", delay
case resp.StatusCode >= 500 || resp.StatusCode == 408:
return Outcome{State: model.Unknown, Code: "provider_outcome_unknown"}
outcome.State, outcome.Code = model.Unknown, "provider_outcome_unknown"
case resp.StatusCode == 401 || resp.StatusCode == 403:
return Outcome{State: model.Failed, Code: "provider_authentication_failed"}
outcome.State, outcome.Code = model.Failed, "provider_authentication_failed"
default:
return Outcome{State: model.Failed, Code: "provider_rejected"}
outcome.State, outcome.Code = model.Failed, "provider_rejected"
}
if outcome.State != model.Accepted {
if readErr != nil || len(details) > maxErrorBody {
outcome.ProviderMessage = "Provider error details unavailable: incomplete or oversized response"
} else {
outcome.ProviderError, outcome.ProviderMessage = providerFailure(details, body, c.key, key)
}
}
return outcome
}