From 03b7ac4de8a336dfa3ae752727ff5603db2dc97a Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 21 Apr 2026 13:54:55 +0200 Subject: [PATCH 1/2] Fall back to anonymous download on 4xx with Maven credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Maven settings.xml credentials are rejected by the remote repository (401/403), retry the JAR download without authentication. Mirrors `MavenPomDownloader.requestAsAuthenticatedOrAnonymous()` and Apache Maven's behavior, so anonymous-accessible artifacts resolve even when configured credentials are invalid. Also fixes two nits observed during troubleshooting: - local cache filename was missing the hyphen before the classifier (`foo-1.0.0recipes.jar` → `foo-1.0.0-recipes.jar`) - download error message omitted the classifier --- .../maven/cache/LocalMavenArtifactCache.java | 2 +- .../utilities/MavenArtifactDownloader.java | 59 +++++++++++++++---- .../MavenArtifactDownloaderTest.java | 57 ++++++++++++++++++ 3 files changed, 104 insertions(+), 14 deletions(-) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java b/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java index 5777978f2d3..b8b34a50690 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/cache/LocalMavenArtifactCache.java @@ -85,7 +85,7 @@ private Path dependencyPath(ResolvedDependency dependency) { return resolvedPath.resolve(dependency.getArtifactId() + "-" + (dependency.getDatedSnapshotVersion() == null ? dependency.getVersion() : dependency.getDatedSnapshotVersion()) + - (dependency.getRequested().getClassifier() == null ? "" : dependency.getRequested().getClassifier()) + + (dependency.getRequested().getClassifier() == null ? "" : "-" + dependency.getRequested().getClassifier()) + ".jar"); } } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java b/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java index b675c991d7c..6659fb10e82 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java @@ -110,15 +110,35 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache, bodyStream = Files.newInputStream(Paths.get(URI.create(uri))); } else { HttpSender.Request.Builder request = applyAuthentication(dependency.getRepository(), httpSender.get(uri)); - try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(request.build())); - InputStream body = response.getBody()) { - if (!response.isSuccessful() || body == null) { - onError.accept(new MavenDownloadingException(String.format("Unable to download dependency %s:%s:%s from %s. Response was %d", - dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), uri, response.getCode()), null, + try { + byte[] responseBytes = null; + int responseCode; + try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(request.build())); + InputStream body = response.getBody()) { + responseCode = response.getCode(); + if (response.isSuccessful() && body != null) { + responseBytes = readAllBytes(body); + } + } + // Fall back to anonymous if authenticated request fails with a 4xx client error + if (responseBytes == null && hasCredentials(dependency.getRepository()) && responseCode >= 400 && responseCode < 500) { + try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(httpSender.get(uri).build())); + InputStream body = response.getBody()) { + responseCode = response.getCode(); + if (response.isSuccessful() && body != null) { + responseBytes = readAllBytes(body); + } + } + } + if (responseBytes == null) { + onError.accept(new MavenDownloadingException(String.format("Unable to download dependency %s:%s:%s%s from %s. Response was %d", + dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), + dependency.getClassifier() == null ? "" : ":" + dependency.getClassifier(), + uri, responseCode), null, dependency.getRequested().getGav())); return null; } - bodyStream = new ByteArrayInputStream(readAllBytes(body)); + bodyStream = new ByteArrayInputStream(responseBytes); } catch (Throwable t) { Throwable cause = t instanceof FailsafeException && t.getCause() != null ? t.getCause() : t; throw new MavenDownloadingException("Unable to download dependency", cause, @@ -130,14 +150,27 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache, } private HttpSender.Request.Builder applyAuthentication(MavenRepository repository, HttpSender.Request.Builder request) { + MavenSettings.Server authInfo = serverIdToServer.get(repository.getId()); + if (authInfo != null && authInfo.getConfiguration() != null && authInfo.getConfiguration().getHttpHeaders() != null) { + for (MavenSettings.HttpHeader header : authInfo.getConfiguration().getHttpHeaders()) { + request.withHeader(header.getName(), header.getValue()); + } + } + String[] credentials = resolveCredentials(repository); + if (credentials != null) { + return request.withBasicAuthentication(credentials[0], credentials[1]); + } + return request; + } + + private boolean hasCredentials(MavenRepository repository) { + return resolveCredentials(repository) != null; + } + + private String @Nullable [] resolveCredentials(MavenRepository repository) { String username, password; MavenSettings.Server authInfo = serverIdToServer.get(repository.getId()); if (authInfo != null) { - if (authInfo.getConfiguration() != null && authInfo.getConfiguration().getHttpHeaders() != null) { - for (MavenSettings.HttpHeader header : authInfo.getConfiguration().getHttpHeaders()) { - request.withHeader(header.getName(), header.getValue()); - } - } username = authInfo.getUsername(); password = authInfo.getPassword(); } else { @@ -146,8 +179,8 @@ private HttpSender.Request.Builder applyAuthentication(MavenRepository repositor } if (username != null && !username.contains("${") && password != null && !password.contains("${")) { - return request.withBasicAuthentication(username, password); + return new String[]{username, password}; } - return request; + return null; } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java index a501181570f..0a553dc814f 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java @@ -132,6 +132,63 @@ void downloadDependenciesWithClassifier(@TempDir Path tempDir) { } } + @Test + void fallsBackToAnonymousWhenServerReturns401(@TempDir Path tempDir) throws Exception { + byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; + + try (MockWebServer mockRepo = new MockWebServer()) { + mockRepo.setDispatcher(new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) { + if (request.getHeader("Authorization") != null) { + return new MockResponse().setResponseCode(401); + } + return new MockResponse().setResponseCode(200) + .setBody(new okio.Buffer().write(jarBytes)); + } + }); + mockRepo.start(); + + String repoUrl = "http://" + mockRepo.getHostName() + ":" + mockRepo.getPort(); + MavenSettings settings = MavenSettings.parse(new Parser.Input( + Path.of("settings.xml"), () -> new ByteArrayInputStream( + //language=xml + """ + + + + mock-repo + bad-user + bad-password + + + + """.getBytes())), new InMemoryExecutionContext()); + + MavenArtifactCache artifactCache = new LocalMavenArtifactCache(tempDir); + AtomicReference error = new AtomicReference<>(); + MavenArtifactDownloader downloader = new MavenArtifactDownloader( + artifactCache, settings, error::set); + + MavenRepository repo = new MavenRepository( + "mock-repo", repoUrl, "true", "false", true, null, null, null, false); + GroupArtifactVersion gav = new GroupArtifactVersion("com.example", "test-lib", "1.0.0"); + ResolvedDependency dep = ResolvedDependency.builder() + .repository(repo) + .gav(new ResolvedGroupArtifactVersion(repoUrl, gav.getGroupId(), gav.getArtifactId(), gav.getVersion(), null)) + .requested(Dependency.builder().gav(gav).build()) + .build(); + + Path artifact = downloader.downloadArtifact(dep); + + assertThat(artifact).isNotNull(); + assertThat(error.get()).isNull(); + assertThat(mockRepo.getRequestCount()).isEqualTo(2); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNotNull(); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNull(); + } + } + @Test void fallsBackToAnonymousWhenCredentialsRejected(@TempDir Path tempDir) throws Exception { byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; // minimal ZIP magic bytes From 8408c6483e2dd2ff9a0619d8857812f67e4d74e3 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 28 Apr 2026 16:07:53 +0200 Subject: [PATCH 2/2] Try anonymous first, retry with credentials on 4xx Mirrors Apache Maven Resolver's DeferredCredentialsProvider behavior: issue an unauthenticated request first, then send credentials only when the server challenges with a 4xx. This matches what users get from running Maven directly, and avoids leaking credentials to public artifacts. --- .../utilities/MavenArtifactDownloader.java | 11 +++++---- .../MavenArtifactDownloaderTest.java | 23 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java b/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java index 6659fb10e82..57a971d708e 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/utilities/MavenArtifactDownloader.java @@ -109,20 +109,21 @@ public MavenArtifactDownloader(MavenArtifactCache mavenArtifactCache, } else if ("file".equals(URI.create(uri).getScheme())) { bodyStream = Files.newInputStream(Paths.get(URI.create(uri))); } else { - HttpSender.Request.Builder request = applyAuthentication(dependency.getRepository(), httpSender.get(uri)); try { + // Try anonymously first, mirroring Apache Maven's DeferredCredentialsProvider behavior byte[] responseBytes = null; int responseCode; - try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(request.build())); + try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(httpSender.get(uri).build())); InputStream body = response.getBody()) { responseCode = response.getCode(); if (response.isSuccessful() && body != null) { responseBytes = readAllBytes(body); } } - // Fall back to anonymous if authenticated request fails with a 4xx client error - if (responseBytes == null && hasCredentials(dependency.getRepository()) && responseCode >= 400 && responseCode < 500) { - try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(httpSender.get(uri).build())); + // Retry with credentials if the anonymous request failed with a 4xx and we have credentials + if (responseBytes == null && responseCode >= 400 && responseCode < 500 && hasCredentials(dependency.getRepository())) { + HttpSender.Request.Builder request = applyAuthentication(dependency.getRepository(), httpSender.get(uri)); + try (HttpSender.Response response = Failsafe.with(retryPolicy).get(() -> httpSender.send(request.build())); InputStream body = response.getBody()) { responseCode = response.getCode(); if (response.isSuccessful() && body != null) { diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java index 0a553dc814f..69dbd0ff0ce 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/utilities/MavenArtifactDownloaderTest.java @@ -133,8 +133,8 @@ void downloadDependenciesWithClassifier(@TempDir Path tempDir) { } @Test - void fallsBackToAnonymousWhenServerReturns401(@TempDir Path tempDir) throws Exception { - byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; + void publicArtifactsResolveAnonymouslyEvenWhenCredentialsAreInvalid(@TempDir Path tempDir) throws Exception { + byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; // minimal ZIP magic bytes try (MockWebServer mockRepo = new MockWebServer()) { mockRepo.setDispatcher(new Dispatcher() { @@ -183,22 +183,21 @@ public MockResponse dispatch(RecordedRequest request) { assertThat(artifact).isNotNull(); assertThat(error.get()).isNull(); - assertThat(mockRepo.getRequestCount()).isEqualTo(2); - assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNotNull(); + assertThat(mockRepo.getRequestCount()).isEqualTo(1); assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNull(); } } @Test - void fallsBackToAnonymousWhenCredentialsRejected(@TempDir Path tempDir) throws Exception { - byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; // minimal ZIP magic bytes + void retriesWithCredentialsWhenAnonymousReturns401(@TempDir Path tempDir) throws Exception { + byte[] jarBytes = {0x50, 0x4B, 0x03, 0x04}; try (MockWebServer mockRepo = new MockWebServer()) { mockRepo.setDispatcher(new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) { - if (request.getHeader("Authorization") != null) { - return new MockResponse().setResponseCode(403); // Throw if used; it should not be called at all + if (request.getHeader("Authorization") == null) { + return new MockResponse().setResponseCode(401); } return new MockResponse().setResponseCode(200) .setBody(new okio.Buffer().write(jarBytes)); @@ -215,8 +214,8 @@ public MockResponse dispatch(RecordedRequest request) { mock-repo - ${placeholder} - ${placeholder} + good-user + good-password @@ -240,7 +239,9 @@ public MockResponse dispatch(RecordedRequest request) { assertThat(artifact).isNotNull(); assertThat(error.get()).isNull(); - assertThat(mockRepo.getRequestCount()).isEqualTo(1); + assertThat(mockRepo.getRequestCount()).isEqualTo(2); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNull(); + assertThat(mockRepo.takeRequest().getHeader("Authorization")).isNotNull(); } } }