From ced83df38ffa670ae66dd48b9171331b7a5a73a6 Mon Sep 17 00:00:00 2001 From: Philipp Garbe Date: Mon, 10 Aug 2026 08:43:35 +0200 Subject: [PATCH] fix(npm): use combined Accept header to support Artifactory upstreams When cooldown is disabled, send: Accept: application/vnd.npm.install-v1+json;q=1.0, application/json;q=0.8 This allows upstreams like JFrog Artifactory that return 406 for the abbreviated packument type to fall back to full JSON metadata, while letting the public npm registry continue to serve the smaller abbreviated format it prefers. When cooldown is enabled, keep sending only application/json because the abbreviated format omits the "time" map required for version age filtering. Fixes #228 Co-Authored-By: Claude Sonnet 4.6 --- internal/handler/npm.go | 11 +++++++---- internal/handler/npm_test.go | 12 ++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/internal/handler/npm.go b/internal/handler/npm.go index ee9b59c..78aa5e4 100644 --- a/internal/handler/npm.go +++ b/internal/handler/npm.go @@ -13,7 +13,7 @@ import ( const ( npmUpstream = "https://registry.npmjs.org" - npmAbbreviatedCT = "application/vnd.npm.install-v1+json" + npmAcceptDefault = "application/vnd.npm.install-v1+json;q=1.0, application/json;q=0.8" scopedParts = 2 // scope + name in scoped packages ) @@ -71,9 +71,12 @@ func (h *NPMHandler) handlePackageMetadata(w http.ResponseWriter, r *http.Reques upstreamURL := fmt.Sprintf("%s/%s", h.upstreamURL, url.PathEscape(packageName)) - // Use abbreviated metadata when cooldown is disabled — it's much smaller - // (e.g. drizzle-orm: 4MB vs 92MB) but lacks the time map needed for cooldown. - accept := npmAbbreviatedCT + // Prefer the smaller abbreviated packument format but include application/json + // as a fallback so upstreams that reject the abbreviated type (e.g. JFrog + // Artifactory, which returns 406) can still respond with full metadata. + // When cooldown is enabled we must use full metadata exclusively because the + // abbreviated format omits the "time" map required for version age filtering. + accept := npmAcceptDefault if h.proxy.Cooldown != nil && h.proxy.Cooldown.Enabled() { accept = contentTypeJSON } diff --git a/internal/handler/npm_test.go b/internal/handler/npm_test.go index e0257dd..fd6f080 100644 --- a/internal/handler/npm_test.go +++ b/internal/handler/npm_test.go @@ -396,7 +396,7 @@ func TestNPMHandlerUsesAbbreviatedMetadata(t *testing.T) { })) defer upstream.Close() - t.Run("no cooldown uses abbreviated metadata", func(t *testing.T) { + t.Run("no cooldown uses combined accept header", func(t *testing.T) { h := &NPMHandler{ proxy: testProxy(), upstreamURL: upstream.URL, @@ -407,12 +407,12 @@ func TestNPMHandlerUsesAbbreviatedMetadata(t *testing.T) { w := httptest.NewRecorder() h.handlePackageMetadata(w, req) - if gotAccept != npmAbbreviatedCT { - t.Errorf("Accept = %q, want abbreviated metadata header", gotAccept) + if gotAccept != npmAcceptDefault { + t.Errorf("Accept = %q, want %q", gotAccept, npmAcceptDefault) } }) - t.Run("cooldown enabled uses full metadata", func(t *testing.T) { + t.Run("cooldown enabled uses full metadata only", func(t *testing.T) { proxy := testProxy() proxy.Cooldown = &cooldown.Config{Default: "3d"} @@ -426,8 +426,8 @@ func TestNPMHandlerUsesAbbreviatedMetadata(t *testing.T) { w := httptest.NewRecorder() h.handlePackageMetadata(w, req) - if gotAccept == npmAbbreviatedCT { - t.Error("cooldown enabled should use full metadata, not abbreviated") + if gotAccept != contentTypeJSON { + t.Errorf("Accept = %q, want %q (cooldown requires full metadata)", gotAccept, contentTypeJSON) } }) }