Skip to content

Commit abce914

Browse files
committed
Polish 'Handle HTTP 407 with clear error message'
See gh-47180
1 parent 5aa841d commit abce914

File tree

7 files changed

+81
-38
lines changed

7 files changed

+81
-38
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@
1717
package org.springframework.boot.buildpack.platform.docker.transport;
1818

1919
import java.net.URI;
20+
import java.nio.charset.StandardCharsets;
21+
22+
import org.apache.hc.core5.http.HttpStatus;
2023

2124
import org.springframework.util.Assert;
25+
import org.springframework.util.ObjectUtils;
2226
import org.springframework.util.StringUtils;
2327

2428
/**
2529
* Exception thrown when a call to the Docker API fails.
2630
*
2731
* @author Phillip Webb
2832
* @author Scott Frederick
33+
* @author Siva Sai Udayagiri
2934
* @since 2.3.0
3035
*/
3136
public class DockerEngineException extends RuntimeException {
@@ -38,9 +43,26 @@ public class DockerEngineException extends RuntimeException {
3843

3944
private final Message responseMessage;
4045

46+
/**
47+
* Create a new {@link DockerEngineException}.
48+
* @param host the host
49+
* @param uri the URI being called
50+
* @param statusCode the status code
51+
* @param reasonPhrase the reason phrase
52+
* @param errors the errors or {@code null}
53+
* @param responseMessage the response message
54+
* @deprecated since 3.4.12 for removal in 4.0.0 since the exception should only be
55+
* thrown by the transport.
56+
*/
57+
@Deprecated(since = "3.4.12", forRemoval = true)
4158
public DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
4259
Message responseMessage) {
43-
super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage));
60+
this(host, uri, statusCode, reasonPhrase, errors, responseMessage, null);
61+
}
62+
63+
DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
64+
Message responseMessage, byte[] content) {
65+
super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage, content));
4466
this.statusCode = statusCode;
4567
this.reasonPhrase = reasonPhrase;
4668
this.errors = errors;
@@ -82,7 +104,7 @@ public Message getResponseMessage() {
82104
}
83105

84106
private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
85-
Message responseMessage) {
107+
Message responseMessage, byte[] content) {
86108
Assert.notNull(host, "Host must not be null");
87109
Assert.notNull(uri, "URI must not be null");
88110
StringBuilder message = new StringBuilder(
@@ -93,6 +115,10 @@ private static String buildMessage(String host, URI uri, int statusCode, String
93115
if (responseMessage != null && StringUtils.hasLength(responseMessage.getMessage())) {
94116
message.append(" and message \"").append(responseMessage.getMessage()).append("\"");
95117
}
118+
else if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED && !ObjectUtils.isEmpty(content)) {
119+
String contentString = new String(content, StandardCharsets.UTF_8);
120+
message.append(" and content \"").append(contentString.trim()).append("\"");
121+
}
96122
if (errors != null && !errors.isEmpty()) {
97123
message.append(" ").append(errors);
98124
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.io.OutputStream;
2323
import java.net.URI;
2424
import java.net.URISyntaxException;
25-
import java.nio.charset.StandardCharsets;
2625

2726
import org.apache.hc.client5.http.classic.HttpClient;
2827
import org.apache.hc.client5.http.classic.methods.HttpDelete;
@@ -37,7 +36,6 @@
3736
import org.apache.hc.core5.http.HttpEntity;
3837
import org.apache.hc.core5.http.HttpHost;
3938
import org.apache.hc.core5.http.HttpRequest;
40-
import org.apache.hc.core5.http.HttpStatus;
4139
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
4240

4341
import org.springframework.boot.buildpack.platform.io.Content;
@@ -162,27 +160,14 @@ private Response execute(HttpUriRequest request) {
162160
beforeExecute(request);
163161
ClassicHttpResponse response = this.client.executeOpen(this.host, request, null);
164162
int statusCode = response.getCode();
165-
166163
if (statusCode >= 400 && statusCode <= 500) {
167164
byte[] content = readContent(response);
168165
response.close();
169-
170-
if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
171-
String detail = (content != null && content.length > 0)
172-
? new String(content, StandardCharsets.UTF_8) : null;
173-
174-
String msg = "Proxy authentication required for host: " + this.host.toHostString() + ", uri: "
175-
+ request.getUri() + (StringUtils.hasText(detail) ? " - " + detail : "");
176-
177-
throw new ProxyAuthenticationException(msg);
178-
}
179-
180166
Errors errors = (statusCode != 500) ? deserializeErrors(content) : null;
181167
Message message = deserializeMessage(content);
182168
throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode,
183-
response.getReasonPhrase(), errors, message);
169+
response.getReasonPhrase(), errors, message, content);
184170
}
185-
186171
return new HttpClientResponse(response);
187172
}
188173
catch (IOException | URISyntaxException ex) {

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
3232
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
3333
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
34-
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
34+
import org.springframework.boot.buildpack.platform.docker.transport.TestDockerEngineException;
3535
import org.springframework.boot.buildpack.platform.docker.type.Binding;
3636
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
3737
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
@@ -313,12 +313,12 @@ void buildInvokesBuilderWithIfNotPresentPullPolicy() throws Exception {
313313
any(), isNull()))
314314
.willAnswer(withPulledImage(runImage));
315315
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))))
316-
.willThrow(
317-
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
316+
.willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null,
317+
null, null))
318318
.willReturn(builderImage);
319319
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
320-
.willThrow(
321-
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
320+
.willThrow(new TestDockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null,
321+
null, null))
322322
.willReturn(runImage);
323323
Builder builder = new Builder(BuildLog.to(out), docker, null);
324324
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT);

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.net.URI;
2020
import java.net.URISyntaxException;
21+
import java.nio.charset.StandardCharsets;
2122
import java.util.Collections;
2223

2324
import org.junit.jupiter.api.Test;
@@ -56,20 +57,20 @@ class DockerEngineExceptionTests {
5657
@Test
5758
void createWhenHostIsNullThrowsException() {
5859
assertThatIllegalArgumentException()
59-
.isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE))
60+
.isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS, NO_MESSAGE, null))
6061
.withMessage("Host must not be null");
6162
}
6263

6364
@Test
6465
void createWhenUriIsNullThrowsException() {
6566
assertThatIllegalArgumentException()
66-
.isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE))
67+
.isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS, NO_MESSAGE, null))
6768
.withMessage("URI must not be null");
6869
}
6970

7071
@Test
7172
void create() {
72-
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE);
73+
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, MESSAGE, null);
7374
assertThat(exception.getMessage()).isEqualTo(
7475
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\" [code: message]");
7576
assertThat(exception.getStatusCode()).isEqualTo(404);
@@ -80,7 +81,7 @@ void create() {
8081

8182
@Test
8283
void createWhenReasonPhraseIsNull() {
83-
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE);
84+
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS, MESSAGE, null);
8485
assertThat(exception.getMessage()).isEqualTo(
8586
"Docker API call to 'docker://localhost/example' failed with status code 404 and message \"response message\" [code: message]");
8687
assertThat(exception.getStatusCode()).isEqualTo(404);
@@ -91,15 +92,16 @@ void createWhenReasonPhraseIsNull() {
9192

9293
@Test
9394
void createWhenErrorsIsNull() {
94-
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE);
95+
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null, MESSAGE, null);
9596
assertThat(exception.getMessage()).isEqualTo(
9697
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\"");
9798
assertThat(exception.getErrors()).isNull();
9899
}
99100

100101
@Test
101102
void createWhenErrorsIsEmpty() {
102-
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE);
103+
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS, MESSAGE,
104+
null);
103105
assertThat(exception.getMessage()).isEqualTo(
104106
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" and message \"response message\"");
105107
assertThat(exception.getStatusCode()).isEqualTo(404);
@@ -109,18 +111,28 @@ void createWhenErrorsIsEmpty() {
109111

110112
@Test
111113
void createWhenMessageIsNull() {
112-
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null);
114+
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, null, null);
113115
assertThat(exception.getMessage()).isEqualTo(
114116
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]");
115117
assertThat(exception.getResponseMessage()).isNull();
116118
}
117119

118120
@Test
119121
void createWhenMessageIsEmpty() {
120-
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE);
122+
DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS, NO_MESSAGE,
123+
null);
121124
assertThat(exception.getMessage()).isEqualTo(
122125
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]");
123126
assertThat(exception.getResponseMessage()).isSameAs(NO_MESSAGE);
124127
}
125128

129+
@Test
130+
void createWhenProxyAuthFailureWithTextContent() {
131+
DockerEngineException exception = new DockerEngineException(HOST, URI, 407, "Proxy Authentication Required",
132+
null, null, "Badness".getBytes(StandardCharsets.UTF_8));
133+
assertThat(exception.getMessage())
134+
.isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 407 "
135+
+ "\"Proxy Authentication Required\" and content \"Badness\"");
136+
}
137+
126138
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.apache.hc.core5.http.HttpEntity;
3434
import org.apache.hc.core5.http.HttpHeaders;
3535
import org.apache.hc.core5.http.HttpHost;
36+
import org.apache.hc.core5.http.HttpStatus;
3637
import org.assertj.core.api.ThrowingConsumer;
3738
import org.junit.jupiter.api.BeforeEach;
3839
import org.junit.jupiter.api.Test;
@@ -322,6 +323,19 @@ void shouldReturnErrorsAndMessage() throws IOException {
322323
});
323324
}
324325

326+
@Test
327+
void shouldReturnErrorsAndConentIfProxyAuthError() throws IOException {
328+
givenClientWillReturnResponse();
329+
given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("proxy-error.txt"));
330+
given(this.response.getCode()).willReturn(HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
331+
assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri))
332+
.satisfies((ex) -> {
333+
assertThat(ex.getErrors()).isNull();
334+
assertThat(ex.getResponseMessage()).isNull();
335+
assertThat(ex.getMessage()).contains("Some kind of procy auth problem!");
336+
});
337+
}
338+
325339
@Test
326340
void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException {
327341
given(this.client.executeOpen(any(HttpHost.class), any(HttpUriRequest.class), isNull()))
Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2025 the original author or authors.
2+
* Copyright 2012-present the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,14 +16,18 @@
1616

1717
package org.springframework.boot.buildpack.platform.docker.transport;
1818

19-
public class ProxyAuthenticationException extends RuntimeException {
19+
import java.net.URI;
2020

21-
public ProxyAuthenticationException(String message) {
22-
super(message);
23-
}
21+
/**
22+
* Subclass of {@link DockerEngineException} for testing.
23+
*
24+
* @author Phillip Webb
25+
*/
26+
public class TestDockerEngineException extends DockerEngineException {
2427

25-
public ProxyAuthenticationException(String message, Throwable cause) {
26-
super(message, cause);
28+
public TestDockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors,
29+
Message responseMessage, byte[] content) {
30+
super(host, uri, statusCode, reasonPhrase, errors, responseMessage, content);
2731
}
2832

2933
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Some kind of procy auth problem!
2+

0 commit comments

Comments
 (0)