Skip to content

Commit a807a07

Browse files
committed
Always try to deserialize message in case of Docker transport errors
Before this commit, if the status code was 4xx or 500, we tried to read the errors object, consuming the http entity. When we tried to deserialize the message, the http entity was already consumed, an IOException has been thrown and null is returned for the message. Now, we read the content in a byte[] and deserialize the errors and the message from that. This ensures that we can read both the errors and the message. Closes gh-44628
1 parent c849caf commit a807a07

File tree

3 files changed

+60
-13
lines changed

3 files changed

+60
-13
lines changed

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

+31-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 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.
@@ -34,7 +34,6 @@
3434
import org.apache.hc.core5.http.HttpEntity;
3535
import org.apache.hc.core5.http.HttpHost;
3636
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
37-
import org.apache.hc.core5.http.message.StatusLine;
3837

3938
import org.springframework.boot.buildpack.platform.io.Content;
4039
import org.springframework.boot.buildpack.platform.io.IOConsumer;
@@ -49,6 +48,7 @@
4948
* @author Phillip Webb
5049
* @author Mike Smithson
5150
* @author Scott Frederick
51+
* @author Moritz Halbritter
5252
*/
5353
abstract class HttpClientTransport implements HttpTransport {
5454

@@ -147,12 +147,12 @@ private Response execute(HttpUriRequest request) {
147147
ClassicHttpResponse response = this.client.executeOpen(this.host, request, null);
148148
int statusCode = response.getCode();
149149
if (statusCode >= 400 && statusCode <= 500) {
150-
HttpEntity entity = response.getEntity();
151-
Errors errors = (statusCode != 500) ? getErrorsFromResponse(entity) : null;
152-
Message message = getMessageFromResponse(entity);
153-
StatusLine statusLine = new StatusLine(response);
150+
byte[] content = readContent(response);
151+
response.close();
152+
Errors errors = (statusCode != 500) ? deserializeErrors(content) : null;
153+
Message message = deserializeMessage(content);
154154
throw new DockerEngineException(this.host.toHostString(), request.getUri(), statusCode,
155-
statusLine.getReasonPhrase(), errors, message);
155+
response.getReasonPhrase(), errors, message);
156156
}
157157
return new HttpClientResponse(response);
158158
}
@@ -161,19 +161,38 @@ private Response execute(HttpUriRequest request) {
161161
}
162162
}
163163

164-
private Errors getErrorsFromResponse(HttpEntity entity) {
164+
private byte[] readContent(ClassicHttpResponse response) throws IOException {
165+
HttpEntity entity = response.getEntity();
166+
if (entity == null) {
167+
return null;
168+
}
169+
try (InputStream stream = entity.getContent()) {
170+
if (stream == null) {
171+
return null;
172+
}
173+
return stream.readAllBytes();
174+
}
175+
}
176+
177+
private Errors deserializeErrors(byte[] content) {
178+
if (content == null) {
179+
return null;
180+
}
165181
try {
166-
return SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
182+
return SharedObjectMapper.get().readValue(content, Errors.class);
167183
}
168184
catch (IOException ex) {
169185
return null;
170186
}
171187
}
172188

173-
private Message getMessageFromResponse(HttpEntity entity) {
189+
private Message deserializeMessage(byte[] content) {
190+
if (content == null) {
191+
return null;
192+
}
174193
try {
175-
return (entity.getContent() != null)
176-
? SharedObjectMapper.get().readValue(entity.getContent(), Message.class) : null;
194+
Message message = SharedObjectMapper.get().readValue(content, Message.class);
195+
return (message.getMessage() != null) ? message : null;
177196
}
178197
catch (IOException ex) {
179198
return null;

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2025 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.
@@ -57,6 +57,7 @@
5757
* @author Phillip Webb
5858
* @author Mike Smithson
5959
* @author Scott Frederick
60+
* @author Moritz Halbritter
6061
*/
6162
@ExtendWith(MockitoExtension.class)
6263
class HttpClientTransportTests {
@@ -309,6 +310,18 @@ void executeWhenResponseIsIn500RangeWithOtherContentShouldThrowDockerException()
309310
});
310311
}
311312

313+
@Test
314+
void shouldReturnErrorsAndMessage() throws IOException {
315+
givenClientWillReturnResponse();
316+
given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("message-and-errors.json"));
317+
given(this.response.getCode()).willReturn(404);
318+
assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri))
319+
.satisfies((ex) -> {
320+
assertThat(ex.getErrors()).hasSize(2);
321+
assertThat(ex.getResponseMessage().getMessage()).contains("test message");
322+
});
323+
}
324+
312325
@Test
313326
void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException {
314327
given(this.client.executeOpen(any(HttpHost.class), any(HttpUriRequest.class), isNull()))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"message": "test message",
3+
"errors": [
4+
{
5+
"code": "TEST1",
6+
"message": "Test One",
7+
"detail": 123
8+
},
9+
{
10+
"code": "TEST2",
11+
"message": "Test Two",
12+
"detail": "fail"
13+
}
14+
]
15+
}

0 commit comments

Comments
 (0)