From 6051e20adfd8274fcd875840557d918547042efd Mon Sep 17 00:00:00 2001 From: Giles Westwood <201443+gilesw@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:31:06 +0100 Subject: [PATCH] Cache Maven checksums and signatures as artifacts Checksum and .asc files beside a versioned artifact are as immutable as the artifact, but were routed through the metadata path: uncached until a first successful fetch, with no retry or circuit breaker. A build pulling hundreds of artifacts sent as many checksum requests straight to Maven Central, which rate-limited the proxy, after which every further checksum request still went upstream and failed. Only maven-metadata.xml and its own checksums stay on the metadata path, since the version listing changes with each release. --- internal/handler/download_test.go | 116 +++++++++++++++++------------- internal/handler/maven.go | 33 +++++---- internal/handler/maven_test.go | 83 +++++++++------------ 3 files changed, 121 insertions(+), 111 deletions(-) diff --git a/internal/handler/download_test.go b/internal/handler/download_test.go index 2797d421..2eece844 100644 --- a/internal/handler/download_test.go +++ b/internal/handler/download_test.go @@ -758,8 +758,8 @@ func TestMavenHandler_MetadataProxied(t *testing.T) { paths := []string{ "/com/google/guava/guava/maven-metadata.xml", - "/com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar.sha1", - "/com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar.md5", + "/com/google/guava/guava/maven-metadata.xml.sha1", + "/com/google/guava/guava/1.0-SNAPSHOT/maven-metadata.xml", } for _, path := range paths { @@ -829,6 +829,47 @@ func TestMavenHandler_ArtifactExtensions(t *testing.T) { } } +func TestMavenHandler_ChecksumsCachedAsArtifacts(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("checksum %s proxied as metadata", r.URL.Path) + })) + defer upstream.Close() + + h := NewMavenHandler(proxy, "http://localhost", "", "") + h.upstreamURL = upstream.URL + proxy.HTTPClient = upstream.Client() + + srv := httptest.NewServer(h.Routes()) + defer srv.Close() + + for _, name := range []string{"lib-1.0.jar.sha1", "lib-1.0.pom.md5", "lib-1.0.jar.asc", "lib-1.0-bin.tar.gz.sha512"} { + path := "/com/example/lib/1.0/" + name + for i := range 2 { + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader("checksum")), + ContentType: "text/plain", + } + fetcher.fetchCalled = false + + resp, err := http.Get(srv.URL + path) + if err != nil { + t.Fatalf("GET %s failed: %v", path, err) + } + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + + if resp.StatusCode != http.StatusOK || string(body) != "checksum" { + t.Errorf("GET %s: status %d body %q", path, resp.StatusCode, body) + } + if fetcher.fetchCalled != (i == 0) { + t.Errorf("GET %s request %d: fetched = %v, want %v", path, i+1, fetcher.fetchCalled, i == 0) + } + } + } +} + func TestMavenHandler_CacheMiss(t *testing.T) { proxy, _, _, fetcher := setupTestProxy(t) fetcher.artifact = &fetch.Artifact{ @@ -929,10 +970,8 @@ func TestMavenHandler_GradlePluginMarkerFallbackAndCache(t *testing.T) { func TestMavenHandler_GradlePluginMarkerMetadataFallback(t *testing.T) { paths := map[string]string{ - "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha1": "sha1", - "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha256": "sha256", - "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.md5": "md5", - "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/maven-metadata.xml": "", + "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/maven-metadata.xml": "", + "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/maven-metadata.xml.sha1": "sha1", } primaryHits := map[string]int{} @@ -990,63 +1029,40 @@ func TestMavenHandler_GradlePluginMarkerMetadataFallback(t *testing.T) { } } -func TestMavenHandler_GradlePluginImplementationMetadataFallback(t *testing.T) { - paths := map[string]string{ - "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha1": "impl-sha1", - "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha256": "impl-sha256", +func TestMavenHandler_GradlePluginChecksumFallbackToPluginPortal(t *testing.T) { + primaryUpstream := "https://repo1.maven.org/maven2" + pluginPortalUpstream := "https://plugins.gradle.org/m2" + paths := []string{ + "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha1", + "/com/diffplug/spotless/com.diffplug.spotless.gradle.plugin/8.4.0/com.diffplug.spotless.gradle.plugin-8.4.0.pom.md5", + "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha1", + "/com/diffplug/spotless/spotless-plugin-gradle/8.4.0/spotless-plugin-gradle-8.4.0.jar.sha256", } - primaryHits := map[string]int{} - pluginHits := map[string]int{} - - primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - primaryHits[r.URL.Path]++ - if _, ok := paths[r.URL.Path]; ok { - http.NotFound(w, r) - return - } - t.Fatalf("unexpected path to primary upstream: %s", r.URL.Path) - })) - defer primary.Close() - - pluginPortal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - pluginHits[r.URL.Path]++ - body, ok := paths[r.URL.Path] - if !ok { - http.NotFound(w, r) - return + for _, reqPath := range paths { + proxy, _, _, fetcher := setupTestProxy(t) + fetcher.fetchErrByURL = map[string]error{primaryUpstream + reqPath: ErrUpstreamNotFound} + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader("checksum")), + ContentType: "text/plain", } - _, _ = io.WriteString(w, body) - })) - defer pluginPortal.Close() - proxy, _, _, _ := setupTestProxy(t) - proxy.HTTPClient = primary.Client() - - h := NewMavenHandler(proxy, "http://localhost", primary.URL, pluginPortal.URL) - srv := httptest.NewServer(h.Routes()) - defer srv.Close() + h := NewMavenHandler(proxy, "http://localhost", primaryUpstream, pluginPortalUpstream) + srv := httptest.NewServer(h.Routes()) - for reqPath, wantBody := range paths { resp, err := http.Get(srv.URL + reqPath) if err != nil { t.Fatalf("GET %s failed: %v", reqPath, err) } body, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() + srv.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("GET %s: status = %d, want %d", reqPath, resp.StatusCode, http.StatusOK) - } - if string(body) != wantBody { - t.Fatalf("GET %s: body = %q, want %q", reqPath, body, wantBody) - } - - if primaryHits[reqPath] == 0 { - t.Fatalf("GET %s did not hit primary upstream", reqPath) + if resp.StatusCode != http.StatusOK || string(body) != "checksum" { + t.Fatalf("GET %s: status %d body %q", reqPath, resp.StatusCode, body) } - if pluginHits[reqPath] == 0 { - t.Fatalf("GET %s did not hit plugin portal fallback", reqPath) + if want := pluginPortalUpstream + reqPath; fetcher.fetchedURL != want { + t.Fatalf("GET %s: fetched URL = %q, want %q", reqPath, fetcher.fetchedURL, want) } } } diff --git a/internal/handler/maven.go b/internal/handler/maven.go index 700c107f..19226209 100644 --- a/internal/handler/maven.go +++ b/internal/handler/maven.go @@ -79,21 +79,19 @@ func (h *MavenHandler) handleRequest(w http.ResponseWriter, r *http.Request) { return } - // Check if this is an artifact file or metadata filename := path.Base(urlPath) - if h.isMetadataFile(filename) { + if h.isVersionListingFile(filename) { h.handleMetadata(w, r, urlPath) return } - if h.isArtifactFile(filename) { - // Cache artifact files + if h.isArtifactFile(filename) || h.isChecksumFile(filename) { h.handleDownload(w, r, urlPath) return } - // Proxy everything else (directory listings, checksums, etc.) + // Proxy everything else (directory listings etc.) h.proxyUpstream(w, r) } @@ -192,14 +190,23 @@ func (h *MavenHandler) isArtifactFile(filename string) bool { return false } -// isMetadataFile returns true if the filename is Maven metadata. -func (h *MavenHandler) isMetadataFile(filename string) bool { - return filename == "maven-metadata.xml" || - strings.HasSuffix(filename, ".sha1") || - strings.HasSuffix(filename, ".sha256") || - strings.HasSuffix(filename, ".sha512") || - strings.HasSuffix(filename, ".md5") || - strings.HasSuffix(filename, ".asc") +// isVersionListingFile matches maven-metadata.xml and its checksums. The +// listing changes with every release, so it is cached for the metadata TTL +// rather than kept like an artifact. +func (h *MavenHandler) isVersionListingFile(filename string) bool { + return strings.HasPrefix(filename, "maven-metadata.xml") +} + +// isChecksumFile matches the checksum and signature files published beside +// an artifact. They are as immutable as the artifact, so they are cached +// with it rather than refetched on every request. +func (h *MavenHandler) isChecksumFile(filename string) bool { + for _, ext := range []string{".sha1", ".sha256", ".sha512", ".md5", ".asc"} { + if strings.HasSuffix(filename, ext) { + return true + } + } + return false } // proxyUpstream forwards a request to Maven Central without caching. diff --git a/internal/handler/maven_test.go b/internal/handler/maven_test.go index 9331a003..beaa8f77 100644 --- a/internal/handler/maven_test.go +++ b/internal/handler/maven_test.go @@ -71,62 +71,49 @@ func TestMavenIsArtifactFile(t *testing.T) { } } -func TestMavenIsMetadataFile(t *testing.T) { +func TestMavenIsVersionListingFile(t *testing.T) { h := &MavenHandler{} tests := []struct { - name string filename string want bool }{ - { - name: "pom is artifact, not metadata", - filename: "com.diffplug.spotless.gradle.plugin-8.4.0.pom", - want: false, - }, - { - name: "pom checksum is metadata", - filename: "com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha1", - want: true, - }, - { - name: "metadata file", - filename: "maven-metadata.xml", - want: true, - }, - { - name: "metadata checksum", - filename: "maven-metadata.xml.sha256", - want: true, - }, - { - name: "jar checksum is metadata", - filename: "guava-32.1.3-jre.jar.sha1", - want: true, - }, - { - name: "asc signature is metadata", - filename: "guava-32.1.3-jre.jar.asc", - want: true, - }, - { - name: "regular jar is not metadata", - filename: "guava-32.1.3-jre.jar", - want: false, - }, - { - name: "pom checksum is metadata", - filename: "guava-32.1.3-jre.pom.sha1", - want: true, - }, + {"maven-metadata.xml", true}, + {"maven-metadata.xml.sha1", true}, + {"maven-metadata.xml.sha256", true}, + {"guava-32.1.3-jre.jar", false}, + {"guava-32.1.3-jre.jar.sha1", false}, + {"com.diffplug.spotless.gradle.plugin-8.4.0.pom", false}, + } + + for _, tt := range tests { + if got := h.isVersionListingFile(tt.filename); got != tt.want { + t.Errorf("isVersionListingFile(%q) = %v, want %v", tt.filename, got, tt.want) + } + } +} + +func TestMavenIsChecksumFile(t *testing.T) { + h := &MavenHandler{} + + tests := []struct { + filename string + want bool + }{ + {"guava-32.1.3-jre.jar.sha1", true}, + {"guava-32.1.3-jre.jar.md5", true}, + {"guava-32.1.3-jre.pom.sha1", true}, + {"guava-32.1.3-jre.jar.asc", true}, + {"apache-maven-3.9.9-bin.tar.gz.sha512", true}, + {"com.diffplug.spotless.gradle.plugin-8.4.0.pom.sha1", true}, + {"guava-32.1.3-jre.jar", false}, + {"guava-32.1.3-jre.pom", false}, + {"maven-metadata.xml", false}, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := h.isMetadataFile(tt.filename) - if got != tt.want { - t.Errorf("isMetadataFile(%q) = %v, want %v", tt.filename, got, tt.want) - } - }) + if got := h.isChecksumFile(tt.filename); got != tt.want { + t.Errorf("isChecksumFile(%q) = %v, want %v", tt.filename, got, tt.want) + } } }