Skip to content

Commit 882b291

Browse files
jadolgclaude
andauthored
Distinguish image-not-found from authentication errors (#216)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 86b7536 commit 882b291

File tree

3 files changed

+48
-9
lines changed

3 files changed

+48
-9
lines changed

index.html

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,14 +368,17 @@
368368
detail: null,
369369
};
370370

371-
// Check for common error patterns
372-
if (
371+
// Check for common error patterns — check backend status-based
372+
// messages first, then fall back to registry error codes.
373+
if (errorMessage.includes("image not found")) {
374+
result.title = "Image Not Found";
375+
result.message = "The requested image does not exist.";
376+
} else if (
373377
errorMessage.includes("MANIFEST_UNKNOWN") ||
374378
errorMessage.includes("manifest unknown")
375379
) {
376380
result.title = "Image Not Found";
377381
result.message = "The requested image tag does not exist.";
378-
// Try to extract the tag
379382
const tagMatch = errorMessage.match(
380383
/unknown tag=([^\s}"]+)/,
381384
);
@@ -388,6 +391,10 @@
388391
) {
389392
result.title = "Repository Not Found";
390393
result.message = "The requested repository does not exist.";
394+
} else if (errorMessage.includes("access denied")) {
395+
result.title = "Access Denied";
396+
result.message =
397+
"The image may not exist, or you may need credentials to access it.";
391398
} else if (
392399
errorMessage.includes("UNAUTHORIZED") ||
393400
errorMessage.includes("unauthorized")

registry.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ const bearerPrefix = "Bearer "
1919
const responseBodyStr = "response body"
2020
const invalidImageReferenceFormat = "invalid image reference: %w"
2121

22+
// ErrImageNotFound is returned when the registry responds with 404 for a manifest request.
23+
type ErrImageNotFound struct {
24+
Image string
25+
}
26+
27+
func (e *ErrImageNotFound) Error() string {
28+
return fmt.Sprintf("image not found: %s", e.Image)
29+
}
30+
2231
// ImageReference represents a parsed Docker image reference
2332
type ImageReference struct {
2433
Registry string
@@ -228,7 +237,7 @@ func (c *RegistryClient) fetchToken(realm, service, scope string, creds Registry
228237
if resp.StatusCode != http.StatusOK {
229238
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
230239
log.WithField("response_body", string(body)).WithField("status_code", resp.StatusCode).Debug("Authentication request failed")
231-
return "", fmt.Errorf("authentication failed with status %d", resp.StatusCode)
240+
return "", fmt.Errorf("authentication failed (status %d): check credentials or verify the image exists", resp.StatusCode)
232241
}
233242

234243
var tokenResp struct {
@@ -328,7 +337,14 @@ func (c *RegistryClient) GetPlatforms(ref ImageReference) ([]Platform, error) {
328337
}
329338
defer closeWithLog(resp.Body, responseBodyStr)
330339

331-
if resp.StatusCode != http.StatusOK {
340+
switch resp.StatusCode {
341+
case http.StatusOK:
342+
// handled below
343+
case http.StatusNotFound:
344+
return nil, &ErrImageNotFound{Image: ref.Repository + ":" + ref.Tag}
345+
case http.StatusUnauthorized, http.StatusForbidden:
346+
return nil, fmt.Errorf("access denied (status %d): check credentials or verify the image exists", resp.StatusCode)
347+
default:
332348
body, _ := io.ReadAll(resp.Body)
333349
return nil, fmt.Errorf("failed to get manifest: %d - %s", resp.StatusCode, string(body))
334350
}
@@ -414,7 +430,14 @@ func (c *RegistryClient) getManifest(ref ImageReference, platform Platform) (*Ma
414430
}
415431
defer closeWithLog(resp.Body, responseBodyStr)
416432

417-
if resp.StatusCode != http.StatusOK {
433+
switch resp.StatusCode {
434+
case http.StatusOK:
435+
// handled below
436+
case http.StatusNotFound:
437+
return nil, &ErrImageNotFound{Image: ref.Repository + ":" + ref.Tag}
438+
case http.StatusUnauthorized, http.StatusForbidden:
439+
return nil, fmt.Errorf("access denied (status %d): check credentials or verify the image exists", resp.StatusCode)
440+
default:
418441
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
419442
return nil, fmt.Errorf("failed to get manifest: %d - %s", resp.StatusCode, string(body))
420443
}

server.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"embed"
66
"encoding/json"
7+
"errors"
78
"fmt"
89
"net"
910
"net/http"
@@ -72,7 +73,7 @@ func (s *Server) Start(ctx context.Context) (*http.Server, error) {
7273
"cache_dir": s.cache.Dir(),
7374
}).Info("Starting server")
7475
go func() {
75-
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
76+
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
7677
log.WithError(err).Fatal("Server error")
7778
}
7879
}()
@@ -153,7 +154,11 @@ func (s *Server) imageHandler(w http.ResponseWriter, r *http.Request) {
153154
"image": imageName,
154155
}).WithError(err).Error("Failed to download image")
155156
errorsTotalMetric.Inc()
156-
writeJSONError(w, "failed to download image", http.StatusInternalServerError)
157+
if notFound, match := errors.AsType[*ErrImageNotFound](err); match {
158+
writeJSONError(w, notFound.Error(), http.StatusNotFound)
159+
} else {
160+
writeJSONError(w, fmt.Sprintf("failed to download image: %v", err), http.StatusInternalServerError)
161+
}
157162
return
158163
}
159164
imagePath := result.(string)
@@ -171,7 +176,11 @@ func (s *Server) platformsHandler(w http.ResponseWriter, r *http.Request) {
171176
platforms, err := GetImagePlatforms(imageName)
172177
if err != nil {
173178
log.WithField("image", imageName).WithError(err).Error("Failed to get platforms")
174-
writeJSONError(w, "failed to get platforms", http.StatusInternalServerError)
179+
if notFound, match := errors.AsType[*ErrImageNotFound](err); match {
180+
writeJSONError(w, notFound.Error(), http.StatusNotFound)
181+
} else {
182+
writeJSONError(w, fmt.Sprintf("failed to get platforms: %v", err), http.StatusInternalServerError)
183+
}
175184
return
176185
}
177186
if platforms == nil {

0 commit comments

Comments
 (0)