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)
+ }
}
}