From 7e174ed7da36452300916a8404482d8c78d1f3dc Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Sun, 19 Apr 2026 22:11:32 -0700 Subject: [PATCH] fix(backend): short-circuit retry.Do on 401/403 auth errors from the registry pull and push wrap per-layer processing in retry.Do with defaultRetryOpts, which retries up to 6 times on exponential 5s..60s backoff. The options didn't set a RetryIf predicate, so every error was treated as transient - including 401 Unauthorized and 403 Forbidden from the registry. Auth errors never recover on retry with the same credentials, so the user sat through ~30-60s of silent backoff before seeing the real failure. Add a predicate that unwraps the error chain looking for oras.land/oras-go/v2/registry/remote/errcode.ErrorResponse (the typed error oras emits for HTTP responses the registry rejected) and returns false for 401/403. Transient HTTP failures (5xx, network resets, registry rate-limit 429, etc.) still retry as before. Fixes #494 Signed-off-by: SAY-5 --- pkg/backend/retry.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pkg/backend/retry.go b/pkg/backend/retry.go index c7494250..b58eddc2 100644 --- a/pkg/backend/retry.go +++ b/pkg/backend/retry.go @@ -17,14 +17,33 @@ package backend import ( + "errors" + "net/http" "time" retry "github.com/avast/retry-go/v4" + "oras.land/oras-go/v2/registry/remote/errcode" ) +// isAuthError reports whether err represents a registry auth failure that +// cannot be fixed by retrying the same request with the same credentials. +// Retrying those wastes the user's time on the ~30s+ exponential backoff +// before the final error surfaces, so callers should short-circuit instead. +func isAuthError(err error) bool { + var respErr *errcode.ErrorResponse + if errors.As(err, &respErr) { + return respErr.StatusCode == http.StatusUnauthorized || + respErr.StatusCode == http.StatusForbidden + } + return false +} + var defaultRetryOpts = []retry.Option{ retry.Attempts(6), retry.DelayType(retry.BackOffDelay), retry.Delay(5 * time.Second), retry.MaxDelay(60 * time.Second), + // Registry auth errors will not recover on retry; fail fast so the user + // sees the real error within seconds instead of 30s+ of silent backoff. + retry.RetryIf(func(err error) bool { return !isAuthError(err) }), }