From 1bd6ffefe0947e4fe1e4847a1e44645166c23546 Mon Sep 17 00:00:00 2001 From: SSUday Date: Fri, 12 Sep 2025 14:11:37 -0400 Subject: [PATCH 1/3] Handle Docker 407 (Proxy Authentication Required) with clear error message (Fixes #46262) Signed-off-by: Siva Sai Udayagiri --- .../docker/transport/HttpClientTransport.java | 23 ++++++++++- .../ProxyAuthenticationException.java | 29 ++++++++++++++ .../ProxyAuthenticationExceptionTests.java | 39 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationException.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index 7f8ecc9e9e29..c2a991d58f8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -146,14 +146,35 @@ private Response execute(HttpUriRequest request) { try { ClassicHttpResponse response = this.client.executeOpen(this.host, request, null); int statusCode = response.getCode(); + if (statusCode >= 400 && statusCode <= 500) { byte[] content = readContent(response); - response.close(); + + if (statusCode == 407) { + response.close(); + + String detail = null; + Message json = deserializeMessage(content); + if (json != null && org.springframework.util.StringUtils.hasText(json.getMessage())) { + detail = json.getMessage(); + } + else { + detail = new String(content, java.nio.charset.StandardCharsets.UTF_8); + } + + String msg = "Proxy authentication required for host: " + this.host.toHostString() + ", uri: " + + request.getUri() + + (org.springframework.util.StringUtils.hasText(detail) ? " - " + detail : ""); + + throw new ProxyAuthenticationException(msg); + } + Errors errors = (statusCode != 500) ? deserializeErrors(content) : null; Message message = deserializeMessage(content); throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode, response.getReasonPhrase(), errors, message); } + return new HttpClientResponse(response); } catch (IOException | URISyntaxException ex) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationException.java new file mode 100644 index 000000000000..3211ec132be0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +public class ProxyAuthenticationException extends RuntimeException { + + public ProxyAuthenticationException(String message) { + super(message); + } + + public ProxyAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java new file mode 100644 index 000000000000..d5fe3d9602e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.transport; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProxyAuthenticationExceptionTests { + + @Test + void constructsWithMessage() { + ProxyAuthenticationException ex = new ProxyAuthenticationException("Proxy auth failed"); + assertThat(ex.getMessage()).isEqualTo("Proxy auth failed"); + } + + @Test + void constructsWithMessageAndCause() { + Exception cause = new Exception("boom"); + ProxyAuthenticationException ex = new ProxyAuthenticationException("Proxy auth failed", cause); + assertThat(ex.getCause()).isSameAs(cause); + assertThat(ex.getMessage()).isEqualTo("Proxy auth failed"); + } + +} From dea1f3cc13b960029b47445102ac43083d3dc154 Mon Sep 17 00:00:00 2001 From: Siva Sai Udayagiri Date: Mon, 22 Sep 2025 13:35:25 -0400 Subject: [PATCH 2/3] Handle Docker 407: always close response on error; use imports; remove FQNs Signed-off-by: Siva Sai Udayagiri --- .../docker/transport/HttpClientTransport.java | 48 +++---------------- .../ProxyAuthenticationExceptionTests.java | 39 --------------- 2 files changed, 7 insertions(+), 80 deletions(-) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index c2a991d58f8c..f8c8f507b871 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -22,6 +22,7 @@ import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.classic.methods.HttpDelete; @@ -65,66 +66,31 @@ protected HttpClientTransport(HttpClient client, HttpHost host) { this.host = host; } - /** - * Perform an HTTP GET operation. - * @param uri the destination URI - * @return the operation response - */ @Override public Response get(URI uri) { return execute(new HttpGet(uri)); } - /** - * Perform an HTTP POST operation. - * @param uri the destination URI - * @return the operation response - */ @Override public Response post(URI uri) { return execute(new HttpPost(uri)); } - /** - * Perform an HTTP POST operation. - * @param uri the destination URI - * @param registryAuth registry authentication credentials - * @return the operation response - */ @Override public Response post(URI uri, String registryAuth) { return execute(new HttpPost(uri), registryAuth); } - /** - * Perform an HTTP POST operation. - * @param uri the destination URI - * @param contentType the content type to write - * @param writer a content writer - * @return the operation response - */ @Override public Response post(URI uri, String contentType, IOConsumer writer) { return execute(new HttpPost(uri), contentType, writer); } - /** - * Perform an HTTP PUT operation. - * @param uri the destination URI - * @param contentType the content type to write - * @param writer a content writer - * @return the operation response - */ @Override public Response put(URI uri, String contentType, IOConsumer writer) { return execute(new HttpPut(uri), contentType, writer); } - /** - * Perform an HTTP DELETE operation. - * @param uri the destination URI - * @return the operation response - */ @Override public Response delete(URI uri) { return execute(new HttpDelete(uri)); @@ -149,22 +115,22 @@ private Response execute(HttpUriRequest request) { if (statusCode >= 400 && statusCode <= 500) { byte[] content = readContent(response); + // Always close the response for error paths + response.close(); if (statusCode == 407) { - response.close(); - String detail = null; Message json = deserializeMessage(content); - if (json != null && org.springframework.util.StringUtils.hasText(json.getMessage())) { + if (json != null && StringUtils.hasText(json.getMessage())) { detail = json.getMessage(); } - else { - detail = new String(content, java.nio.charset.StandardCharsets.UTF_8); + else if (content != null && content.length > 0) { + detail = new String(content, StandardCharsets.UTF_8); } String msg = "Proxy authentication required for host: " + this.host.toHostString() + ", uri: " + request.getUri() - + (org.springframework.util.StringUtils.hasText(detail) ? " - " + detail : ""); + + (StringUtils.hasText(detail) ? " - " + detail : ""); throw new ProxyAuthenticationException(msg); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java deleted file mode 100644 index d5fe3d9602e0..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ProxyAuthenticationExceptionTests.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.buildpack.platform.docker.transport; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class ProxyAuthenticationExceptionTests { - - @Test - void constructsWithMessage() { - ProxyAuthenticationException ex = new ProxyAuthenticationException("Proxy auth failed"); - assertThat(ex.getMessage()).isEqualTo("Proxy auth failed"); - } - - @Test - void constructsWithMessageAndCause() { - Exception cause = new Exception("boom"); - ProxyAuthenticationException ex = new ProxyAuthenticationException("Proxy auth failed", cause); - assertThat(ex.getCause()).isSameAs(cause); - assertThat(ex.getMessage()).isEqualTo("Proxy auth failed"); - } - -} From b31f951a373204190a44476799b3d8cd59e8f0f0 Mon Sep 17 00:00:00 2001 From: Siva Sai Udayagiri Date: Fri, 3 Oct 2025 15:51:00 -0400 Subject: [PATCH 3/3] Handle Docker 407: always close error responses; use HttpStatus constant; restore Javadoc; remove comment Signed-off-by: Siva Sai Udayagiri --- .../docker/transport/HttpClientTransport.java | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index f8c8f507b871..3bdaaa45a28d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -34,6 +34,7 @@ import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; import org.springframework.boot.buildpack.platform.io.Content; @@ -66,31 +67,66 @@ protected HttpClientTransport(HttpClient client, HttpHost host) { this.host = host; } + /** + * Perform an HTTP GET operation. + * @param uri the destination URI + * @return the operation response + */ @Override public Response get(URI uri) { return execute(new HttpGet(uri)); } + /** + * Perform an HTTP POST operation. + * @param uri the destination URI + * @return the operation response + */ @Override public Response post(URI uri) { return execute(new HttpPost(uri)); } + /** + * Perform an HTTP POST operation. + * @param uri the destination URI + * @param registryAuth registry authentication credentials + * @return the operation response + */ @Override public Response post(URI uri, String registryAuth) { return execute(new HttpPost(uri), registryAuth); } + /** + * Perform an HTTP POST operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + */ @Override public Response post(URI uri, String contentType, IOConsumer writer) { return execute(new HttpPost(uri), contentType, writer); } + /** + * Perform an HTTP PUT operation. + * @param uri the destination URI + * @param contentType the content type to write + * @param writer a content writer + * @return the operation response + */ @Override public Response put(URI uri, String contentType, IOConsumer writer) { return execute(new HttpPut(uri), contentType, writer); } + /** + * Perform an HTTP DELETE operation. + * @param uri the destination URI + * @return the operation response + */ @Override public Response delete(URI uri) { return execute(new HttpDelete(uri)); @@ -115,11 +151,14 @@ private Response execute(HttpUriRequest request) { if (statusCode >= 400 && statusCode <= 500) { byte[] content = readContent(response); - // Always close the response for error paths response.close(); - if (statusCode == 407) { + if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { String detail = null; + + // Some Docker endpoints may still send a JSON body; prefer it if + // present, + // otherwise fall back to plain text. Message json = deserializeMessage(content); if (json != null && StringUtils.hasText(json.getMessage())) { detail = json.getMessage(); @@ -129,8 +168,7 @@ else if (content != null && content.length > 0) { } String msg = "Proxy authentication required for host: " + this.host.toHostString() + ", uri: " - + request.getUri() - + (StringUtils.hasText(detail) ? " - " + detail : ""); + + request.getUri() + (StringUtils.hasText(detail) ? " - " + detail : ""); throw new ProxyAuthenticationException(msg); }