diff --git a/go.mod b/go.mod index b84d213..8984fd2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/git-pkgs/cooldown v0.1.1 github.com/git-pkgs/enrichment v0.6.0 github.com/git-pkgs/purl v0.1.14 - github.com/git-pkgs/registries v0.6.2 + github.com/git-pkgs/registries v0.6.3 github.com/git-pkgs/spdx v0.1.4 github.com/git-pkgs/vers v0.2.6 github.com/git-pkgs/vulns v0.1.6 diff --git a/go.sum b/go.sum index 6da21d4..402d9b8 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY= github.com/git-pkgs/pom v0.1.5/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk= github.com/git-pkgs/purl v0.1.14 h1:GgqwiBNS0eKJqJ/gabUBEC10Xhpj6UX12kfNzujaeYc= github.com/git-pkgs/purl v0.1.14/go.mod h1:8oCcdcYZA/e1B33e7Ylju6azboTKjdqf3ybcbQj6I/o= -github.com/git-pkgs/registries v0.6.2 h1:26G5zW6Q7x1CSfNkaEqEjRMJiA4JwfdKOCJ7Qm+u0a8= -github.com/git-pkgs/registries v0.6.2/go.mod h1:GR0Bu6nC3NQe6f7lfDoEVqAnoQkMocf4M98B12a7B3E= +github.com/git-pkgs/registries v0.6.3 h1:7sveeeMS2lgXtcqNYAA3bwaT7H9CQs0uhy6wQSxz3Js= +github.com/git-pkgs/registries v0.6.3/go.mod h1:j4o50ii/vD9Z42/nFeQASt15BPolADIejeFmmqK4njo= github.com/git-pkgs/spdx v0.1.4 h1:eQ0waEV3uUeItpWAOvdN1K1rL9hTgsU7fF74r1mDXMs= github.com/git-pkgs/spdx v0.1.4/go.mod h1:cqRoZcvl530s/W+oGNvwjt4ODN8T1W6D/20MUZEFdto= github.com/git-pkgs/vers v0.2.6 h1:IelZd7BP/JhzTloUTDY67nehUgoYva3g9viqAMCHJg8= diff --git a/internal/handler/cargo.go b/internal/handler/cargo.go index bf424e2..abc0d1f 100644 --- a/internal/handler/cargo.go +++ b/internal/handler/cargo.go @@ -191,8 +191,7 @@ func (h *CargoHandler) handleDownload(w http.ResponseWriter, r *http.Request) { result, err := h.proxy.GetOrFetchArtifact(r.Context(), "cargo", name, version, filename) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch crate", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch crate") return } diff --git a/internal/handler/composer.go b/internal/handler/composer.go index 23deba5..45935b7 100644 --- a/internal/handler/composer.go +++ b/internal/handler/composer.go @@ -346,8 +346,7 @@ func (h *ComposerHandler) handleDownload(w http.ResponseWriter, r *http.Request) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "composer", packageName, version, filename, downloadURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/conan.go b/internal/handler/conan.go index 53f6428..c0476f9 100644 --- a/internal/handler/conan.go +++ b/internal/handler/conan.go @@ -84,8 +84,7 @@ func (h *ConanHandler) handleRecipeFile(w http.ResponseWriter, r *http.Request) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch file", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch file") return } @@ -122,8 +121,7 @@ func (h *ConanHandler) handlePackageFile(w http.ResponseWriter, r *http.Request) result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conan", packageName, storageVersion, storageFilename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch file", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch file") return } diff --git a/internal/handler/conda.go b/internal/handler/conda.go index a8632e8..224c25f 100644 --- a/internal/handler/conda.go +++ b/internal/handler/conda.go @@ -72,8 +72,7 @@ func (h *CondaHandler) handleDownload(w http.ResponseWriter, r *http.Request) { result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "conda", packageName, version, filename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/container.go b/internal/handler/container.go index 8ba5e97..0710ed1 100644 --- a/internal/handler/container.go +++ b/internal/handler/container.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "errors" "fmt" "io" "net/http" @@ -117,8 +118,12 @@ func (h *ContainerHandler) handleBlobDownload(w http.ResponseWriter, r *http.Req ) if err != nil { + if errors.Is(err, ErrUpstreamNotFound) { + h.containerError(w, http.StatusNotFound, "BLOB_UNKNOWN", "blob unknown to registry") + return + } h.proxy.Logger.Error("failed to fetch blob", "error", err) - h.containerError(w, http.StatusBadGateway, "BLOB_UNKNOWN", "failed to fetch blob") + h.containerError(w, http.StatusBadGateway, "INTERNAL_ERROR", "failed to fetch blob") return } diff --git a/internal/handler/cran.go b/internal/handler/cran.go index 0ecd2a3..2fc4fab 100644 --- a/internal/handler/cran.go +++ b/internal/handler/cran.go @@ -72,8 +72,7 @@ func (h *CRANHandler) handleSourceDownload(w http.ResponseWriter, r *http.Reques result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, version, filename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } @@ -107,8 +106,7 @@ func (h *CRANHandler) handleBinaryDownload(w http.ResponseWriter, r *http.Reques result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "cran", name, storageVersion, filename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/debian.go b/internal/handler/debian.go index b767f6d..9a4aaab 100644 --- a/internal/handler/debian.go +++ b/internal/handler/debian.go @@ -81,8 +81,7 @@ func (h *DebianHandler) handlePackageDownload(w http.ResponseWriter, r *http.Req result, err := h.proxy.GetOrFetchArtifactFromURL( r.Context(), "deb", name, version, filename, downloadURL) if err != nil { - h.proxy.Logger.Error("failed to get debian package", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/filename_download.go b/internal/handler/filename_download.go new file mode 100644 index 0000000..bedec16 --- /dev/null +++ b/internal/handler/filename_download.go @@ -0,0 +1,39 @@ +package handler + +import ( + "net/http" + "strings" +) + +type filenameDownload struct { + ecosystem string + suffix string + parseErr string + fetchErr string + parse func(string) (name, version string) +} + +func (p *Proxy) handleFilenameDownload(w http.ResponseWriter, r *http.Request, d filenameDownload) { + filename := r.PathValue("filename") + if filename == "" || !strings.HasSuffix(filename, d.suffix) { + http.Error(w, "invalid filename", http.StatusBadRequest) + return + } + + name, version := d.parse(filename) + if name == "" || version == "" { + http.Error(w, d.parseErr, http.StatusBadRequest) + return + } + + p.Logger.Info(d.ecosystem+" download request", + "name", name, "version", version, "filename", filename) + + result, err := p.GetOrFetchArtifact(r.Context(), d.ecosystem, name, version, filename) + if err != nil { + p.serveArtifactError(w, err, d.fetchErr) + return + } + + ServeArtifact(w, result) +} diff --git a/internal/handler/gem.go b/internal/handler/gem.go index a3714d6..260568a 100644 --- a/internal/handler/gem.go +++ b/internal/handler/gem.go @@ -58,30 +58,13 @@ func (h *GemHandler) Routes() http.Handler { // handleDownload serves a gem file, fetching and caching from upstream if needed. func (h *GemHandler) handleDownload(w http.ResponseWriter, r *http.Request) { - filename := r.PathValue("filename") - if filename == "" || !strings.HasSuffix(filename, ".gem") { - http.Error(w, "invalid filename", http.StatusBadRequest) - return - } - - // Extract name and version from filename (e.g., "rails-7.1.0.gem") - name, version := h.parseGemFilename(filename) - if name == "" || version == "" { - http.Error(w, "could not parse gem filename", http.StatusBadRequest) - return - } - - h.proxy.Logger.Info("gem download request", - "name", name, "version", version, "filename", filename) - - result, err := h.proxy.GetOrFetchArtifact(r.Context(), "gem", name, version, filename) - if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch gem", http.StatusBadGateway) - return - } - - ServeArtifact(w, result) + h.proxy.handleFilenameDownload(w, r, filenameDownload{ + ecosystem: "gem", + suffix: ".gem", + parseErr: "could not parse gem filename", + fetchErr: "failed to fetch gem", + parse: h.parseGemFilename, + }) } // parseGemFilename extracts name and version from a gem filename. diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 202426d..e62292f 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -251,6 +251,9 @@ func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, fil // Resolve download URL info, err := p.Resolver.Resolve(ctx, ecosystem, name, version) if err != nil { + if errors.Is(err, fetch.ErrNotFound) { + return nil, ErrUpstreamNotFound + } return nil, fmt.Errorf("resolving download URL: %w", err) } @@ -270,6 +273,9 @@ func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, fil if err != nil { metrics.RecordUpstreamFetch(ecosystem, fetchDuration) metrics.RecordUpstreamError(ecosystem, "fetch_failed") + if errors.Is(err, fetch.ErrNotFound) { + return nil, ErrUpstreamNotFound + } return nil, fmt.Errorf("fetching from upstream: %w", err) } metrics.RecordUpstreamFetch(ecosystem, fetchDuration) @@ -451,7 +457,18 @@ func JSONError(w http.ResponseWriter, status int, message string) { } // ErrUpstreamNotFound indicates the upstream returned 404. -var ErrUpstreamNotFound = fmt.Errorf("upstream: not found") +var ErrUpstreamNotFound = fmt.Errorf("upstream: %w", fetch.ErrNotFound) + +// serveArtifactError writes response for a failed fetch: +// 404 when upstream reports artifact missing, 502 otherwise. +func (p *Proxy) serveArtifactError(w http.ResponseWriter, err error, clientMsg string) { + if errors.Is(err, ErrUpstreamNotFound) { + http.Error(w, "not found", http.StatusNotFound) + return + } + p.Logger.Error("failed to get artifact", "error", err) + http.Error(w, clientMsg, http.StatusBadGateway) +} // errStale304 is returned when upstream sends 304 but the cached file is missing. var errStale304 = fmt.Errorf("upstream returned 304 but cached file is missing") @@ -795,6 +812,9 @@ func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, versi artifact, err := p.Fetcher.FetchWithHeaders(ctx, downloadURL, headers) if err != nil { + if errors.Is(err, fetch.ErrNotFound) { + return nil, ErrUpstreamNotFound + } return nil, fmt.Errorf("fetching from upstream: %w", err) } diff --git a/internal/handler/hex.go b/internal/handler/hex.go index 6ebc176..2ff4f0f 100644 --- a/internal/handler/hex.go +++ b/internal/handler/hex.go @@ -53,30 +53,13 @@ func (h *HexHandler) Routes() http.Handler { // handleDownload serves a package tarball, fetching and caching from upstream if needed. func (h *HexHandler) handleDownload(w http.ResponseWriter, r *http.Request) { - filename := r.PathValue("filename") - if filename == "" || !strings.HasSuffix(filename, ".tar") { - http.Error(w, "invalid filename", http.StatusBadRequest) - return - } - - // Extract name and version from filename (e.g., "phoenix-1.7.10.tar") - name, version := h.parseTarballFilename(filename) - if name == "" || version == "" { - http.Error(w, "could not parse tarball filename", http.StatusBadRequest) - return - } - - h.proxy.Logger.Info("hex download request", - "name", name, "version", version, "filename", filename) - - result, err := h.proxy.GetOrFetchArtifact(r.Context(), "hex", name, version, filename) - if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) - return - } - - ServeArtifact(w, result) + h.proxy.handleFilenameDownload(w, r, filenameDownload{ + ecosystem: "hex", + suffix: ".tar", + parseErr: "could not parse tarball filename", + fetchErr: "failed to fetch package", + parse: h.parseTarballFilename, + }) } // parseTarballFilename extracts name and version from a hex tarball filename. diff --git a/internal/handler/julia.go b/internal/handler/julia.go index 08b1fdf..0fed8c9 100644 --- a/internal/handler/julia.go +++ b/internal/handler/julia.go @@ -90,8 +90,7 @@ func (h *JuliaHandler) handleRegistry(w http.ResponseWriter, r *http.Request) { upstreamURL := h.upstreamURL + r.URL.Path result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaRegistryName, hash, hash+".tar.gz", upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get registry", "error", err) - http.Error(w, "failed to fetch registry", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch registry") return } @@ -119,8 +118,7 @@ func (h *JuliaHandler) handlePackage(w http.ResponseWriter, r *http.Request) { upstreamURL := h.upstreamURL + r.URL.Path result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", name, hash, hash+".tar.gz", upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get package", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } @@ -141,8 +139,7 @@ func (h *JuliaHandler) handleArtifact(w http.ResponseWriter, r *http.Request) { upstreamURL := h.upstreamURL + r.URL.Path result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "julia", juliaArtifactName, hash, hash+".tar.gz", upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch artifact", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch artifact") return } diff --git a/internal/handler/maven.go b/internal/handler/maven.go index c423645..10e551e 100644 --- a/internal/handler/maven.go +++ b/internal/handler/maven.go @@ -130,12 +130,7 @@ func (h *MavenHandler) handleDownload(w http.ResponseWriter, r *http.Request, ur } } if err != nil { - if errors.Is(err, ErrUpstreamNotFound) { - http.Error(w, "not found", http.StatusNotFound) - return - } - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch artifact", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch artifact") return } diff --git a/internal/handler/notfound_ecosystems_test.go b/internal/handler/notfound_ecosystems_test.go new file mode 100644 index 0000000..3c2cecf --- /dev/null +++ b/internal/handler/notfound_ecosystems_test.go @@ -0,0 +1,151 @@ +package handler + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/git-pkgs/registries/fetch" +) + +func TestArtifactDownloadUpstreamNotFoundReturns404(t *testing.T) { + tests := []struct { + name string + path string + handler func(p *Proxy) http.Handler + }{ + {"debian", "/pool/main/n/nginx/nginx_1.18.0-6_amd64.deb", + func(p *Proxy) http.Handler { return NewDebianHandler(p, "http://localhost").Routes() }}, + {"rpm", "/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm", + func(p *Proxy) http.Handler { return NewRPMHandler(p, "http://localhost").Routes() }}, + {"nuget", "/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg", + func(p *Proxy) http.Handler { return NewNuGetHandler(p, "http://localhost").Routes() }}, + {"pypi", "/packages/packages/ab/cd/ef0123456789/requests-2.31.0-py3-none-any.whl", + func(p *Proxy) http.Handler { return NewPyPIHandler(p, "http://localhost").Routes() }}, + {"cran", "/src/contrib/ggplot2_3.4.4.tar.gz", + func(p *Proxy) http.Handler { return NewCRANHandler(p, "http://localhost").Routes() }}, + {"conda", "/conda-forge/linux-64/numpy-1.26.0-py311_0.tar.bz2", + func(p *Proxy) http.Handler { return NewCondaHandler(p, "http://localhost").Routes() }}, + {"conan", "/v1/files/zlib/1.3.1/_/_/0/recipe/conan_sources.tgz", + func(p *Proxy) http.Handler { return NewConanHandler(p, "http://localhost").Routes() }}, + {"gem", "/gems/rails-7.1.0.gem", + func(p *Proxy) http.Handler { return NewGemHandler(p, "http://localhost").Routes() }}, + {"hex", "/tarballs/phoenix-1.7.10.tar", + func(p *Proxy) http.Handler { return NewHexHandler(p, "http://localhost").Routes() }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErr = fetch.ErrNotFound + + srv := httptest.NewServer(tt.handler(proxy)) + defer srv.Close() + + resp, err := http.Get(srv.URL + tt.path) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("want 404 for missing upstream artifact, got %d", resp.StatusCode) + } + }) + } +} + +func TestJuliaPackageUpstreamNotFoundReturns404(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErr = fetch.ErrNotFound + + dead := httptest.NewServer(http.NotFoundHandler()) + defer dead.Close() + + h := NewJuliaHandler(proxy, "http://localhost") + h.upstreamURL = dead.URL + + srv := httptest.NewServer(h.Routes()) + defer srv.Close() + + resp, err := http.Get(srv.URL + + "/package/7876af07-990d-54b4-ab0e-23690620f79a/0123456789abcdef0123456789abcdef01234567") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("want 404 for missing upstream package, got %d", resp.StatusCode) + } +} + +func TestComposerDownloadUpstreamNotFoundReturns404(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErr = fetch.ErrNotFound + + meta := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/p2/monolog/monolog.json" { + _, _ = w.Write([]byte(`{ + "packages": { + "monolog/monolog": [ + {"version": "2.9.1", "dist": {"url": "https://example.com/monolog-2.9.1.zip", "type": "zip"}} + ] + } + }`)) + return + } + http.NotFound(w, r) + })) + defer meta.Close() + + h := &ComposerHandler{proxy: proxy, repoURL: meta.URL, proxyURL: "http://localhost"} + srv := httptest.NewServer(h.Routes()) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/files/monolog/monolog/2.9.1/monolog-2.9.1.zip") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("want 404 for missing upstream dist, got %d", resp.StatusCode) + } +} + +func TestContainerBlobUpstreamNotFoundReturns404(t *testing.T) { + authServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"token": "test-token-123"}`)) + })) + defer authServer.Close() + + proxy, _, _, _ := setupTestProxy(t) + proxy.Fetcher = &mockFetcherWithHeaders{ + fetchFn: func(_ context.Context, _ string, _ http.Header) (*fetch.Artifact, error) { + return nil, fetch.ErrNotFound + }, + } + + h := &ContainerHandler{ + proxy: proxy, + registryURL: "https://registry-1.docker.io", + authURL: authServer.URL, + proxyURL: "http://localhost:8080", + } + + req := httptest.NewRequest(http.MethodGet, + "/library/nginx/blobs/sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd", nil) + w := httptest.NewRecorder() + h.Routes().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("want 404 for missing upstream blob, got %d; body: %s", w.Code, w.Body.String()) + } + if !strings.Contains(w.Body.String(), "BLOB_UNKNOWN") { + t.Errorf("want BLOB_UNKNOWN error code in body, got: %s", w.Body.String()) + } +} diff --git a/internal/handler/notfound_test.go b/internal/handler/notfound_test.go new file mode 100644 index 0000000..9ea38ac --- /dev/null +++ b/internal/handler/notfound_test.go @@ -0,0 +1,83 @@ +package handler + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/git-pkgs/registries/fetch" +) + +func TestErrUpstreamNotFoundWrapsFetchErrNotFound(t *testing.T) { + if !errors.Is(ErrUpstreamNotFound, fetch.ErrNotFound) { + t.Fatal("ErrUpstreamNotFound does not wrap fetch.ErrNotFound") + } +} + +func TestGetOrFetchArtifactFromURL_NotFound(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErr = fetch.ErrNotFound + + _, err := proxy.GetOrFetchArtifactFromURL(context.Background(), + "maven", "org.example:missing", "1.0", "missing-1.0.jar", + "http://upstream.test/org/example/missing/1.0/missing-1.0.jar") + + if !errors.Is(err, ErrUpstreamNotFound) { + t.Fatalf("want ErrUpstreamNotFound, got %v", err) + } +} + +func TestMavenHandler_UpstreamNotFoundReturns404(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErr = fetch.ErrNotFound + + h := NewMavenHandler(proxy, "http://localhost", "http://upstream.test", "http://portal.test") + srv := httptest.NewServer(h.Routes()) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/org/example/missing/1.0/missing-1.0.jar") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusNotFound { + t.Errorf("want 404 for missing upstream artifact, got %d", resp.StatusCode) + } +} + +func TestMavenHandler_PluginPortalFallback(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErrByURL = map[string]error{ + "http://upstream.test/org/example/plugin/1.0/plugin-1.0.jar": fetch.ErrNotFound, + } + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader("portal artifact")), + ContentType: "application/java-archive", + } + + h := NewMavenHandler(proxy, "http://localhost", "http://upstream.test", "http://portal.test") + srv := httptest.NewServer(h.Routes()) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/org/example/plugin/1.0/plugin-1.0.jar") + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("want 200 via plugin portal fallback, got %d", resp.StatusCode) + } + body, _ := io.ReadAll(resp.Body) + if string(body) != "portal artifact" { + t.Errorf("want portal artifact body, got %q", body) + } + if fetcher.fetchedURL != "http://portal.test/org/example/plugin/1.0/plugin-1.0.jar" { + t.Errorf("fallback did not hit plugin portal, last URL: %s", fetcher.fetchedURL) + } +} diff --git a/internal/handler/npm.go b/internal/handler/npm.go index 06f539e..a3ed910 100644 --- a/internal/handler/npm.go +++ b/internal/handler/npm.go @@ -263,6 +263,10 @@ func (h *NPMHandler) handleDownload(w http.ResponseWriter, r *http.Request) { result, err := h.proxy.GetOrFetchArtifact(r.Context(), "npm", packageName, version, filename) if err != nil { + if errors.Is(err, ErrUpstreamNotFound) { + JSONError(w, http.StatusNotFound, "package not found") + return + } h.proxy.Logger.Error("failed to get artifact", "error", err) JSONError(w, http.StatusBadGateway, "failed to fetch package") return diff --git a/internal/handler/nuget.go b/internal/handler/nuget.go index 3e6373a..4785e40 100644 --- a/internal/handler/nuget.go +++ b/internal/handler/nuget.go @@ -314,8 +314,7 @@ func (h *NuGetHandler) handleDownload(w http.ResponseWriter, r *http.Request) { result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "nuget", name, version, filename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/pub.go b/internal/handler/pub.go index 2971ddf..e5ca199 100644 --- a/internal/handler/pub.go +++ b/internal/handler/pub.go @@ -67,8 +67,7 @@ func (h *PubHandler) handleDownload(w http.ResponseWriter, r *http.Request) { result, err := h.proxy.GetOrFetchArtifact(r.Context(), "pub", name, version, filename) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/pypi.go b/internal/handler/pypi.go index 0cf39fb..f5a0232 100644 --- a/internal/handler/pypi.go +++ b/internal/handler/pypi.go @@ -426,8 +426,7 @@ func (h *PyPIHandler) handleDownload(w http.ResponseWriter, r *http.Request) { result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "pypi", name, version, filename, upstreamURL) if err != nil { - h.proxy.Logger.Error("failed to get artifact", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return } diff --git a/internal/handler/rpm.go b/internal/handler/rpm.go index 6440d0f..a5752ec 100644 --- a/internal/handler/rpm.go +++ b/internal/handler/rpm.go @@ -83,8 +83,7 @@ func (h *RPMHandler) handlePackageDownload(w http.ResponseWriter, r *http.Reques result, err := h.proxy.GetOrFetchArtifactFromURL( r.Context(), "rpm", name, version, filename, downloadURL) if err != nil { - h.proxy.Logger.Error("failed to get rpm package", "error", err) - http.Error(w, "failed to fetch package", http.StatusBadGateway) + h.proxy.serveArtifactError(w, err, "failed to fetch package") return }