ory/kratos · warning · wrapped sentinel error
ErrUnexpectedStatusCode
ErrUnexpectedStatusCode
Error message
ErrUnexpectedStatusCode wrapped: %d (unexpected status code from HIBP API)
What it means
After the HIBP range request succeeds at the transport level, fetch checks that the HTTP status is 200. Any other status code (e.g. 429 rate limit, 5xx) is wrapped in ErrUnexpectedStatusCode. HIBP only returns the hash-suffix list on 200, so any other code means the result is unusable.
Solutions
- Set ignore_network_errors: true (also covers this error class) so password validation degrades gracefully.
- Rely on the built-in hash cache (s.hashes) and ensure the instance is long-lived rather than restarted per request.
- Check the wrapped status code: 429 implies rate limiting — reduce request volume or add a paid HIBP key via an API proxy.
- Retry later; if persistent, monitor status.api.pwnedpasswords.com for outages.
Defensive patterns
Strategy: fallback
Try / catch
if errors.Is(err, strategy.ErrUnexpectedStatusCode) {
log.WithError(err).Warn("HIBP returned non-200; skipping breach check")
if cfg.IgnoreNetworkErrors { return allowed }
} Prevention
- Set ignore_network_errors: true so 429/5xx from HIBP don't block signups.
- Keep instances warm so the prefix cache absorbs repeated lookups.
- Add alerting on the wrapped status code to detect persistent 429/5xx.
- Throttle signup-triggered validations if running at high volume.
When it happens
Trigger: fetch receives a response with status != 200 from the HIBP API — most commonly HTTP 429 when the unauthenticated rate limit is exceeded, or 5xx during HIBP outages.
Common situations: High-traffic signup flows hammering the HIBP API without caching, deployments sharing IPs behind NAT hitting rate limits, HIBP service degradation.
Related errors
AI-assisted analysis of ory/kratos@b86338da04 (2026-09-07).
Data as JSON: /api/errors/6f36a66a84bf54b8.
Report an issue: GitHub.
Appendix: source
Thrown at selfservice/strategy/password/validator.go:139
}
return greatestLength
}
func (s *DefaultPasswordValidator) fetch(ctx context.Context, hpw []byte, apiDNSName string) (int64, error) {
prefix := fmt.Sprintf("%X", hpw)[0:5]
loc := fmt.Sprintf("https://%s/range/%s", apiDNSName, prefix)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", loc, nil)
if err != nil {
return 0, err
}
res, err := s.reg.HTTPClient(ctx, httpx.ResilientClientWithConnectionTimeout(time.Second)).Do(req)
if err != nil {
return 0, errors.Wrapf(ErrNetworkFailure, "%s", err)
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusOK {
return 0, errors.Wrapf(ErrUnexpectedStatusCode, "%d", res.StatusCode)
}
var thisCount int64
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
row := sc.Text()
result := strings.Split(strings.TrimSpace(row), ":")
// We assume a count of 1. HIBP API sometimes responds without the
// colon, so we just assume that the leak count is one.
//
// See https://github.com/ory/kratos/issues/2145
count := int64(1)
if len(result) == 2 {
count, err = strconv.ParseInt(strings.ReplaceAll(result[1], ",", ""), 10, 64)
if err != nil {
return 0, errors.WithStack(herodot.ErrUpstreamError().WithReasonf("Expected password hash to contain a count formatted as int but got: %s", result[1]))View on GitHub (pinned to b86338da04)