Merge pull request #47180 from siva-sai-udaygiri

* pr/47180:
  Polish 'Handle HTTP 407 with clear error message'
  Handle HTTP 407 with clear error message

Closes gh-47180
This commit is contained in:
Phillip Webb
2025-10-31 11:52:20 -07:00
7 changed files with 103 additions and 16 deletions
@@ -17,8 +17,12 @@
package org.springframework.boot.buildpack.platform.docker.transport;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import org.apache.hc.core5.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -26,6 +30,7 @@ import org.springframework.util.StringUtils;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Siva Sai Udayagiri
* @since 2.3.0
*/
public class DockerEngineException extends RuntimeException {
@@ -38,9 +43,26 @@ public class DockerEngineException extends RuntimeException {
private final Message responseMessage;
/**
* Create a new {@link DockerEngineException}.
* @param host the host
* @param uri the URI being called
* @param statusCode the status code
* @param reasonPhrase the reason phrase
* @param errors the errors or {@code null}
* @param responseMessage the response message
* @deprecated since 3.4.12 for removal in 4.0.0 since the exception should only be
* thrown by the transport.
*/
@Deprecated(since = "3.4.12", forRemoval = true)
public DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
Message responseMessage) {
super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage));
this(host, uri, statusCode, reasonPhrase, errors, responseMessage, null);
}
DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
Message responseMessage, byte[] content) {
super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage, content));
this.statusCode = statusCode;
this.reasonPhrase = reasonPhrase;
this.errors = errors;
@@ -82,7 +104,7 @@ public class DockerEngineException extends RuntimeException {
}
private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
Message responseMessage) {
Message responseMessage, byte[] content) {
Assert.notNull(host, "Host must not be null");
Assert.notNull(uri, "URI must not be null");
StringBuilder message = new StringBuilder(
@@ -93,6 +115,10 @@ public class DockerEngineException extends RuntimeException {
if (responseMessage != null && StringUtils.hasLength(responseMessage.getMessage())) {
message.append(" and message \"").append(responseMessage.getMessage()).append("\"");
}
else if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED && !ObjectUtils.isEmpty(content)) {
String contentString = new String(content, StandardCharsets.UTF_8);
message.append(" and content \"").append(contentString.trim()).append("\"");
}
if (errors != null && !errors.isEmpty()) {
message.append(" ").append(errors);
}
@@ -166,7 +166,7 @@ abstract class HttpClientTransport implements HttpTransport {
Errors errors = (statusCode != 500) ? deserializeErrors(content) : null;
Message message = deserializeMessage(content);
throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode,
response.getReasonPhrase(), errors, message);
response.getReasonPhrase(), errors, message, content);
}
return new HttpClientResponse(response);
}
@@ -31,7 +31,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.transport.TestDockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
@@ -313,12 +313,12 @@ class BuilderTests {
any(), isNull()))
.willAnswer(withPulledImage(runImage));
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
.willThrow(
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
.willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null,
null, null))
.willReturn(builderImage);
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
.willThrow(
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
.willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null,
null, null))
.willReturn(runImage);
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT);
@@ -18,6 +18,7 @@ package org.springframework.boot.buildpack.platform.docker.transport;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import org.junit.jupiter.api.Test;
@@ -56,20 +57,20 @@ class DockerEngineExceptionTests {
@Test
void createWhenHostIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE))
.isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE, null))
.withMessage("Host must not be null");
}
@Test
void createWhenUriIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE))
.isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE, null))
.withMessage("URI must not be null");
}
@Test
void create() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE);
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE, null);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\" [code: message]");
assertThat(exception.getStatusCode()).isEqualTo(404);
@@ -80,7 +81,7 @@ class DockerEngineExceptionTests {
@Test
void createWhenReasonPhraseIsNull() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE);
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE, null);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 and message \"response message\" [code: message]");
assertThat(exception.getStatusCode()).isEqualTo(404);
@@ -91,7 +92,7 @@ class DockerEngineExceptionTests {
@Test
void createWhenErrorsIsNull() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE);
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE, null);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\"");
assertThat(exception.getErrors()).isNull();
@@ -99,7 +100,8 @@ class DockerEngineExceptionTests {
@Test
void createWhenErrorsIsEmpty() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE);
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE,
null);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\"");
assertThat(exception.getStatusCode()).isEqualTo(404);
@@ -109,7 +111,7 @@ class DockerEngineExceptionTests {
@Test
void createWhenMessageIsNull() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null);
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null, null);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]");
assertThat(exception.getResponseMessage()).isNull();
@@ -117,10 +119,20 @@ class DockerEngineExceptionTests {
@Test
void createWhenMessageIsEmpty() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE);
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE,
null);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]");
assertThat(exception.getResponseMessage()).isSameAs(NO_MESSAGE);
}
@Test
void createWhenProxyAuthFailureWithTextContent() {
DockerEngineException exception = new DockerEngineException(HOST, URI, 407, "Proxy Authentication Required",
null, null, "Badness".getBytes(StandardCharsets.UTF_8));
assertThat(exception.getMessage())
.isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 407 "
+ "\"Proxy Authentication Required\" and content \"Badness\"");
}
}
@@ -33,6 +33,7 @@ import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpStatus;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -322,6 +323,19 @@ class HttpClientTransportTests {
});
}
@Test
void shouldReturnErrorsAndConentIfProxyAuthError() throws IOException {
givenClientWillReturnResponse();
given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("proxy-error.txt"));
given(this.response.getCode()).willReturn(HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri))
.satisfies((ex) -> {
assertThat(ex.getErrors()).isNull();
assertThat(ex.getResponseMessage()).isNull();
assertThat(ex.getMessage()).contains("Some kind of procy auth problem!");
});
}
@Test
void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException {
given(this.client.executeOpen(any(HttpHost.class), any(HttpUriRequest.class), isNull()))
@@ -0,0 +1,33 @@
/*
* Copyright 2012-present 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 java.net.URI;
/**
* Subclass of {@link DockerEngineException} for testing.
*
* @author Phillip Webb
*/
public class TestDockerEngineException extends DockerEngineException {
public TestDockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
Message responseMessage, byte[] content) {
super(host, uri, statusCode, reasonPhrase, errors, responseMessage, content);
}
}