Skip to content

Commit 04a40a4

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. This is a cherry-pick of the changes in d5b2836 which were lost in a forward-merge. Fixes gh-23957
1 parent 7f02be4 commit 04a40a4

File tree

3 files changed

+78
-16
lines changed

3 files changed

+78
-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;
@@ -132,8 +132,7 @@ public Response delete(URI uri) {
132132

133133
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
134134
IOConsumer<OutputStream> writer) {
135-
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
136-
request.setEntity(new WritableHttpEntity(writer));
135+
request.setEntity(new WritableHttpEntity(contentType, writer));
137136
return execute(request);
138137
}
139138

@@ -193,7 +192,8 @@ private static class WritableHttpEntity extends AbstractHttpEntity {
193192

194193
private final IOConsumer<OutputStream> writer;
195194

196-
WritableHttpEntity(IOConsumer<OutputStream> writer) {
195+
WritableHttpEntity(String contentType, IOConsumer<OutputStream> writer) {
196+
setContentType(contentType);
197197
this.writer = writer;
198198
}
199199

@@ -204,6 +204,9 @@ public boolean isRepeatable() {
204204

205205
@Override
206206
public long getContentLength() {
207+
if (this.contentType != null && this.contentType.getValue().equals("application/json")) {
208+
return calculateStringContentLength();
209+
}
207210
return -1;
208211
}
209212

@@ -222,6 +225,17 @@ public boolean isStreaming() {
222225
return true;
223226
}
224227

228+
private int calculateStringContentLength() {
229+
try {
230+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
231+
this.writer.accept(bytes);
232+
return bytes.toByteArray().length;
233+
}
234+
catch (IOException ex) {
235+
return -1;
236+
}
237+
}
238+
225239
}
226240

227241
/**

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
@@ -337,10 +337,10 @@ void createCreatesContainer() throws Exception {
337337
.willReturn(responseOf("create-container-response.json"));
338338
ContainerReference containerReference = this.api.create(config);
339339
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
340-
ByteArrayOutputStream out = new ByteArrayOutputStream();
341340
verify(http()).post(any(), any(), this.writer.capture());
341+
ByteArrayOutputStream out = new ByteArrayOutputStream();
342342
this.writer.getValue().accept(out);
343-
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
343+
assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
344344
}
345345

346346
@Test
@@ -359,10 +359,10 @@ void createWhenHasContentContainerWithContent() throws Exception {
359359
given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
360360
ContainerReference containerReference = this.api.create(config, content);
361361
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
362-
ByteArrayOutputStream out = new ByteArrayOutputStream();
363362
verify(http()).post(any(), any(), this.writer.capture());
363+
ByteArrayOutputStream out = new ByteArrayOutputStream();
364364
this.writer.getValue().accept(out);
365-
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
365+
assertThat(out.toByteArray().length).isEqualTo(config.toString().length());
366366
verify(http()).put(any(), any(), this.writer.capture());
367367
this.writer.getValue().accept(out);
368368
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: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ class HttpClientTransportTests {
6464

6565
private static final String APPLICATION_JSON = "application/json";
6666

67+
private static final String APPLICATION_X_TAR = "application/x-tar";
68+
6769
@Mock
6870
private CloseableHttpClient client;
6971

@@ -155,44 +157,90 @@ void postWithEmptyRegistryAuthShouldExecuteHttpPostWithoutHeader() throws Except
155157
}
156158

157159
@Test
158-
void postWithContentShouldExecuteHttpPost() throws Exception {
160+
void postWithJsonContentShouldExecuteHttpPost() throws Exception {
161+
String content = "test";
159162
givenClientWillReturnResponse();
160163
given(this.entity.getContent()).willReturn(this.content);
161164
given(this.statusLine.getStatusCode()).willReturn(200);
162165
Response response = this.http.post(this.uri, APPLICATION_JSON,
163-
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
166+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
167+
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
168+
HttpUriRequest request = this.requestCaptor.getValue();
169+
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
170+
assertThat(request).isInstanceOf(HttpPost.class);
171+
assertThat(request.getURI()).isEqualTo(this.uri);
172+
assertThat(entity.isRepeatable()).isFalse();
173+
assertThat(entity.getContentLength()).isEqualTo(content.length());
174+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON);
175+
assertThat(entity.isStreaming()).isTrue();
176+
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
177+
assertThat(writeToString(entity)).isEqualTo(content);
178+
assertThat(response.getContent()).isSameAs(this.content);
179+
}
180+
181+
@Test
182+
void postWithArchiveContentShouldExecuteHttpPost() throws Exception {
183+
String content = "test";
184+
givenClientWillReturnResponse();
185+
given(this.entity.getContent()).willReturn(this.content);
186+
given(this.statusLine.getStatusCode()).willReturn(200);
187+
Response response = this.http.post(this.uri, APPLICATION_X_TAR,
188+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
164189
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
165190
HttpUriRequest request = this.requestCaptor.getValue();
166191
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
167192
assertThat(request).isInstanceOf(HttpPost.class);
168193
assertThat(request.getURI()).isEqualTo(this.uri);
169-
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
170194
assertThat(entity.isRepeatable()).isFalse();
171195
assertThat(entity.getContentLength()).isEqualTo(-1);
196+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
172197
assertThat(entity.isStreaming()).isTrue();
173198
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
174-
assertThat(writeToString(entity)).isEqualTo("test");
199+
assertThat(writeToString(entity)).isEqualTo(content);
175200
assertThat(response.getContent()).isSameAs(this.content);
176201
}
177202

178203
@Test
179-
void putWithContentShouldExecuteHttpPut() throws Exception {
204+
void putWithJsonContentShouldExecuteHttpPut() throws Exception {
205+
String content = "test";
180206
givenClientWillReturnResponse();
181207
given(this.entity.getContent()).willReturn(this.content);
182208
given(this.statusLine.getStatusCode()).willReturn(200);
183209
Response response = this.http.put(this.uri, APPLICATION_JSON,
184-
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
210+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
211+
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
212+
HttpUriRequest request = this.requestCaptor.getValue();
213+
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
214+
assertThat(request).isInstanceOf(HttpPut.class);
215+
assertThat(request.getURI()).isEqualTo(this.uri);
216+
assertThat(entity.isRepeatable()).isFalse();
217+
assertThat(entity.getContentLength()).isEqualTo(content.length());
218+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_JSON);
219+
assertThat(entity.isStreaming()).isTrue();
220+
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
221+
assertThat(writeToString(entity)).isEqualTo(content);
222+
assertThat(response.getContent()).isSameAs(this.content);
223+
}
224+
225+
@Test
226+
void putWithArchiveContentShouldExecuteHttpPut() throws Exception {
227+
String content = "test";
228+
givenClientWillReturnResponse();
229+
given(this.entity.getContent()).willReturn(this.content);
230+
given(this.statusLine.getStatusCode()).willReturn(200);
231+
Response response = this.http.put(this.uri, APPLICATION_X_TAR,
232+
(out) -> StreamUtils.copy(content, StandardCharsets.UTF_8, out));
185233
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
186234
HttpUriRequest request = this.requestCaptor.getValue();
187235
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
188236
assertThat(request).isInstanceOf(HttpPut.class);
189237
assertThat(request.getURI()).isEqualTo(this.uri);
190-
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
191238
assertThat(entity.isRepeatable()).isFalse();
192239
assertThat(entity.getContentLength()).isEqualTo(-1);
240+
assertThat(entity.getContentType().getValue()).isEqualTo(APPLICATION_X_TAR);
193241
assertThat(entity.isStreaming()).isTrue();
194242
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(entity::getContent);
195-
assertThat(writeToString(entity)).isEqualTo("test");
243+
assertThat(writeToString(entity)).isEqualTo(content);
196244
assertThat(response.getContent()).isSameAs(this.content);
197245
}
198246

0 commit comments

Comments
 (0)