Skip to content

Commit d5b2836

Browse files
Provide content-length header to Docker API calls
Docker daemon authorization plugins reject POST or PUT requests that have a content type `application/json` header but no content length header. This commit ensures that a content length header is provided in these cases. Fixes gh-22840
1 parent d79c23e commit d5b2836

File tree

3 files changed

+76
-16
lines changed

3 files changed

+76
-16
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616

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

19+
import java.io.ByteArrayOutputStream;
1920
import java.io.IOException;
2021
import java.io.InputStream;
2122
import java.io.OutputStream;
2223
import java.net.URI;
2324

2425
import org.apache.http.HttpEntity;
25-
import org.apache.http.HttpHeaders;
2626
import org.apache.http.HttpHost;
2727
import org.apache.http.StatusLine;
2828
import org.apache.http.client.HttpClient;
@@ -118,8 +118,7 @@ public Response delete(URI uri) {
118118

119119
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
120120
IOConsumer<OutputStream> writer) {
121-
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
122-
request.setEntity(new WritableHttpEntity(writer));
121+
request.setEntity(new WritableHttpEntity(contentType, writer));
123122
return execute(request);
124123
}
125124

@@ -172,7 +171,8 @@ private static class WritableHttpEntity extends AbstractHttpEntity {
172171

173172
private final IOConsumer<OutputStream> writer;
174173

175-
WritableHttpEntity(IOConsumer<OutputStream> writer) {
174+
WritableHttpEntity(String contentType, IOConsumer<OutputStream> writer) {
175+
setContentType(contentType);
176176
this.writer = writer;
177177
}
178178

@@ -183,6 +183,9 @@ public boolean isRepeatable() {
183183

184184
@Override
185185
public long getContentLength() {
186+
if (this.contentType != null && this.contentType.getValue().equals("application/json")) {
187+
return calculateStringContentLength();
188+
}
186189
return -1;
187190
}
188191

@@ -201,6 +204,17 @@ public boolean isStreaming() {
201204
return true;
202205
}
203206

207+
private int calculateStringContentLength() {
208+
try {
209+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
210+
this.writer.accept(bytes);
211+
return bytes.toByteArray().length;
212+
}
213+
catch (IOException ex) {
214+
return -1;
215+
}
216+
}
217+
204218
}
205219

206220
/**

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,10 @@ void createCreatesContainer() throws Exception {
262262
.willReturn(responseOf("create-container-response.json"));
263263
ContainerReference containerReference = this.api.create(config);
264264
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
265-
ByteArrayOutputStream out = new ByteArrayOutputStream();
266265
verify(http()).post(any(), any(), this.writer.capture());
266+
ByteArrayOutputStream out = new ByteArrayOutputStream();
267267
this.writer.getValue().accept(out);
268-
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
268+
assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
269269
}
270270

271271
@Test
@@ -284,10 +284,10 @@ void createWhenHasContentContainerWithContent() throws Exception {
284284
given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
285285
ContainerReference containerReference = this.api.create(config, content);
286286
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
287-
ByteArrayOutputStream out = new ByteArrayOutputStream();
288287
verify(http()).post(any(), any(), this.writer.capture());
288+
ByteArrayOutputStream out = new ByteArrayOutputStream();
289289
this.writer.getValue().accept(out);
290-
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
290+
assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
291291
verify(http()).put(any(), any(), this.writer.capture());
292292
this.writer.getValue().accept(out);
293293
assertThat(out.toByteArray()).hasSizeGreaterThan(2000);

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

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class HttpClientTransportTests {
6262

6363
private static final String APPLICATION_JSON = "application/json";
6464

65+
private static final String APPLICATION_X_TAR = "application/x-tar";
66+
6567
@Mock
6668
private CloseableHttpClient client;
6769

@@ -124,42 +126,86 @@ void postShouldExecuteHttpPost() throws Exception {
124126
}
125127

126128
@Test
127-
void postWithContentShouldExecuteHttpPost() throws Exception {
129+
void postWithJsonContentShouldExecuteHttpPost() throws Exception {
130+
String content = "test";
128131
given(this.entity.getContent()).willReturn(this.content);
129132
given(this.statusLine.getStatusCode()).willReturn(200);
130133
Response response = this.http.post(this.uri, APPLICATION_JSON,
131-
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
134+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
135+
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
136+
HttpUriRequest request = this.requestCaptor.getValue();
137+
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
138+
assertThat(request).isInstanceOf(HttpPost.class);
139+
assertThat(request.getURI()).isEqualTo(this.uri);
140+
assertThat(entity.isRepeatable()).isFalse();
141+
assertThat(entity.getContentLength()).isEqualTo(content.length());
142+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON);
143+
assertThat(entity.isStreaming()).isTrue();
144+
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
145+
assertThat(writeToString(entity)).isEqualTo(content);
146+
assertThat(response.getContent()).isSameAs(this.content);
147+
}
148+
149+
@Test
150+
void postWithArchiveContentShouldExecuteHttpPost() throws Exception {
151+
String content = "test";
152+
given(this.entity.getContent()).willReturn(this.content);
153+
given(this.statusLine.getStatusCode()).willReturn(200);
154+
Response response = this.http.post(this.uri, APPLICATION_X_TAR,
155+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
132156
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
133157
HttpUriRequest request = this.requestCaptor.getValue();
134158
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
135159
assertThat(request).isInstanceOf(HttpPost.class);
136160
assertThat(request.getURI()).isEqualTo(this.uri);
137-
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
138161
assertThat(entity.isRepeatable()).isFalse();
139162
assertThat(entity.getContentLength()).isEqualTo(-1);
163+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
140164
assertThat(entity.isStreaming()).isTrue();
141165
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
142-
assertThat(writeToString(entity)).isEqualTo("test");
166+
assertThat(writeToString(entity)).isEqualTo(content);
143167
assertThat(response.getContent()).isSameAs(this.content);
144168
}
145169

146170
@Test
147-
void putWithContentShouldExecuteHttpPut() throws Exception {
171+
void putWithJsonContentShouldExecuteHttpPut() throws Exception {
172+
String content = "test";
148173
given(this.entity.getContent()).willReturn(this.content);
149174
given(this.statusLine.getStatusCode()).willReturn(200);
150175
Response response = this.http.put(this.uri, APPLICATION_JSON,
151-
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
176+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
177+
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
178+
HttpUriRequest request = this.requestCaptor.getValue();
179+
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
180+
assertThat(request).isInstanceOf(HttpPut.class);
181+
assertThat(request.getURI()).isEqualTo(this.uri);
182+
assertThat(entity.isRepeatable()).isFalse();
183+
assertThat(entity.getContentLength()).isEqualTo(content.length());
184+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON);
185+
assertThat(entity.isStreaming()).isTrue();
186+
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
187+
assertThat(writeToString(entity)).isEqualTo(content);
188+
assertThat(response.getContent()).isSameAs(this.content);
189+
}
190+
191+
@Test
192+
void putWithArchiveContentShouldExecuteHttpPut() throws Exception {
193+
String content = "test";
194+
given(this.entity.getContent()).willReturn(this.content);
195+
given(this.statusLine.getStatusCode()).willReturn(200);
196+
Response response = this.http.put(this.uri, APPLICATION_X_TAR,
197+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
152198
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
153199
HttpUriRequest request = this.requestCaptor.getValue();
154200
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
155201
assertThat(request).isInstanceOf(HttpPut.class);
156202
assertThat(request.getURI()).isEqualTo(this.uri);
157-
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
158203
assertThat(entity.isRepeatable()).isFalse();
159204
assertThat(entity.getContentLength()).isEqualTo(-1);
205+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
160206
assertThat(entity.isStreaming()).isTrue();
161207
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
162-
assertThat(writeToString(entity)).isEqualTo("test");
208+
assertThat(writeToString(entity)).isEqualTo(content);
163209
assertThat(response.getContent()).isSameAs(this.content);
164210
}
165211

0 commit comments

Comments
 (0)