Skip to content

Commit 033bebf

Browse files
committed
Remove buffering from ClientHttpRequest implementations
This commit ensures that ClientHttpRequest implementations implement StreamingHttpOutputMessage, so that they do not expose an OutputStream, but store a handle capable of writing to a stream instead. Closes gh-30557
1 parent 1ce22bd commit 033bebf

21 files changed

+421
-711
lines changed

spring-web/src/main/java/org/springframework/http/client/AbstractBufferingClientHttpRequest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2023 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,11 +16,11 @@
1616

1717
package org.springframework.http.client;
1818

19-
import java.io.ByteArrayOutputStream;
2019
import java.io.IOException;
2120
import java.io.OutputStream;
2221

2322
import org.springframework.http.HttpHeaders;
23+
import org.springframework.util.FastByteArrayOutputStream;
2424

2525
/**
2626
* Base implementation of {@link ClientHttpRequest} that buffers output
@@ -31,7 +31,7 @@
3131
*/
3232
abstract class AbstractBufferingClientHttpRequest extends AbstractClientHttpRequest {
3333

34-
private ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream(1024);
34+
private final FastByteArrayOutputStream bufferedOutput = new FastByteArrayOutputStream(1024);
3535

3636

3737
@Override
@@ -41,12 +41,12 @@ protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
4141

4242
@Override
4343
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
44-
byte[] bytes = this.bufferedOutput.toByteArray();
44+
byte[] bytes = this.bufferedOutput.toByteArrayUnsafe();
4545
if (headers.getContentLength() < 0) {
4646
headers.setContentLength(bytes.length);
4747
}
4848
ClientHttpResponse result = executeInternal(headers, bytes);
49-
this.bufferedOutput = new ByteArrayOutputStream(0);
49+
this.bufferedOutput.reset();
5050
return result;
5151
}
5252

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
22+
import org.springframework.http.HttpHeaders;
23+
import org.springframework.http.StreamingHttpOutputMessage;
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.FastByteArrayOutputStream;
27+
28+
/**
29+
* Abstract base for {@link ClientHttpRequest} that also implement
30+
* {@link StreamingHttpOutputMessage}. Ensures that headers and
31+
* body are not written multiple times.
32+
*
33+
* @author Arjen Poutsma
34+
* @since 6.1
35+
*/
36+
abstract class AbstractStreamingClientHttpRequest extends AbstractClientHttpRequest
37+
implements StreamingHttpOutputMessage {
38+
39+
@Nullable
40+
private Body body;
41+
42+
@Nullable
43+
private FastByteArrayOutputStream bodyStream;
44+
45+
46+
@Override
47+
protected final OutputStream getBodyInternal(HttpHeaders headers) {
48+
Assert.state(this.body == null, "Invoke either getBody or setBody; not both");
49+
50+
if (this.bodyStream == null) {
51+
this.bodyStream = new FastByteArrayOutputStream(1024);
52+
}
53+
return this.bodyStream;
54+
}
55+
56+
@Override
57+
public final void setBody(Body body) {
58+
Assert.notNull(body, "Body must not be null");
59+
assertNotExecuted();
60+
Assert.state(this.bodyStream == null, "Invoke either getBody or setBody; not both");
61+
62+
this.body = body;
63+
}
64+
65+
@Override
66+
protected final ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
67+
if (this.body == null && this.bodyStream != null) {
68+
this.body = outputStream -> this.bodyStream.writeTo(outputStream);
69+
}
70+
return executeInternal(headers, this.body);
71+
}
72+
73+
74+
/**
75+
* Abstract template method that writes the given headers and content to the HTTP request.
76+
* @param headers the HTTP headers
77+
* @param body the HTTP body, may be {@code null} if no body was {@linkplain #setBody(Body) set}
78+
* @return the response object for the executed request
79+
* @since 6.1
80+
*/
81+
protected abstract ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException;
82+
83+
}

spring-web/src/main/java/org/springframework/http/client/BufferingClientHttpRequestWrapper.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -21,6 +21,7 @@
2121

2222
import org.springframework.http.HttpHeaders;
2323
import org.springframework.http.HttpMethod;
24+
import org.springframework.http.StreamingHttpOutputMessage;
2425
import org.springframework.util.StreamUtils;
2526

2627
/**
@@ -52,7 +53,14 @@ public URI getURI() {
5253
@Override
5354
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
5455
this.request.getHeaders().putAll(headers);
55-
StreamUtils.copy(bufferedOutput, this.request.getBody());
56+
57+
if (this.request instanceof StreamingHttpOutputMessage streamingHttpOutputMessage) {
58+
streamingHttpOutputMessage.setBody(outputStream -> StreamUtils.copy(bufferedOutput, outputStream));
59+
}
60+
else {
61+
StreamUtils.copy(bufferedOutput, this.request.getBody());
62+
}
63+
5664
ClientHttpResponse response = this.request.execute();
5765
return new BufferingClientHttpResponseWrapper(response);
5866
}

spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -17,21 +17,24 @@
1717
package org.springframework.http.client;
1818

1919
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.OutputStream;
2022
import java.net.URI;
2123
import java.net.URISyntaxException;
24+
import java.util.List;
25+
import java.util.Set;
2226

2327
import org.apache.hc.client5.http.classic.HttpClient;
28+
import org.apache.hc.core5.function.Supplier;
2429
import org.apache.hc.core5.http.ClassicHttpRequest;
2530
import org.apache.hc.core5.http.ClassicHttpResponse;
26-
import org.apache.hc.core5.http.ContentType;
31+
import org.apache.hc.core5.http.Header;
2732
import org.apache.hc.core5.http.HttpEntity;
28-
import org.apache.hc.core5.http.HttpResponse;
29-
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
3033
import org.apache.hc.core5.http.protocol.HttpContext;
3134

3235
import org.springframework.http.HttpHeaders;
3336
import org.springframework.http.HttpMethod;
34-
import org.springframework.util.Assert;
37+
import org.springframework.lang.Nullable;
3538
import org.springframework.util.StringUtils;
3639

3740
/**
@@ -46,7 +49,7 @@
4649
* @since 3.1
4750
* @see HttpComponentsClientHttpRequestFactory#createRequest(URI, HttpMethod)
4851
*/
49-
final class HttpComponentsClientHttpRequest extends AbstractBufferingClientHttpRequest {
52+
final class HttpComponentsClientHttpRequest extends AbstractStreamingClientHttpRequest {
5053

5154
private final HttpClient httpClient;
5255

@@ -81,19 +84,16 @@ HttpContext getHttpContext() {
8184
return this.httpContext;
8285
}
8386

84-
85-
@SuppressWarnings("deprecation") // execute(ClassicHttpRequest, HttpContext)
8687
@Override
87-
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
88+
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
8889
addHeaders(this.httpRequest, headers);
8990

90-
ContentType contentType = ContentType.parse(headers.getFirst(HttpHeaders.CONTENT_TYPE));
91-
HttpEntity requestEntity = new ByteArrayEntity(bufferedOutput, contentType);
92-
this.httpRequest.setEntity(requestEntity);
93-
HttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
94-
Assert.isInstanceOf(ClassicHttpResponse.class, httpResponse,
95-
"HttpResponse not an instance of ClassicHttpResponse");
96-
return new HttpComponentsClientHttpResponse((ClassicHttpResponse) httpResponse);
91+
if (body != null) {
92+
HttpEntity requestEntity = new BodyEntity(headers, body);
93+
this.httpRequest.setEntity(requestEntity);
94+
}
95+
ClassicHttpResponse httpResponse = this.httpClient.executeOpen(null, this.httpRequest, this.httpContext);
96+
return new HttpComponentsClientHttpResponse(httpResponse);
9797
}
9898

9999
/**
@@ -116,4 +116,78 @@ else if (!HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(headerName) &&
116116
});
117117
}
118118

119+
120+
private static class BodyEntity implements HttpEntity {
121+
122+
private final HttpHeaders headers;
123+
124+
private final Body body;
125+
126+
127+
public BodyEntity(HttpHeaders headers, Body body) {
128+
this.headers = headers;
129+
this.body = body;
130+
}
131+
132+
133+
@Override
134+
public long getContentLength() {
135+
return this.headers.getContentLength();
136+
}
137+
138+
@Override
139+
@Nullable
140+
public String getContentType() {
141+
return this.headers.getFirst(HttpHeaders.CONTENT_TYPE);
142+
}
143+
144+
@Override
145+
public InputStream getContent() {
146+
throw new UnsupportedOperationException();
147+
}
148+
149+
@Override
150+
public void writeTo(OutputStream outStream) throws IOException {
151+
this.body.writeTo(outStream);
152+
}
153+
154+
@Override
155+
public boolean isRepeatable() {
156+
return false;
157+
}
158+
159+
@Override
160+
public boolean isStreaming() {
161+
return true;
162+
}
163+
164+
@Override
165+
@Nullable
166+
public Supplier<List<? extends Header>> getTrailers() {
167+
return null;
168+
}
169+
170+
@Override
171+
@Nullable
172+
public String getContentEncoding() {
173+
return this.headers.getFirst(HttpHeaders.CONTENT_ENCODING);
174+
}
175+
176+
@Override
177+
public boolean isChunked() {
178+
return false;
179+
}
180+
181+
@Override
182+
@Nullable
183+
public Set<String> getTrailerNames() {
184+
return null;
185+
}
186+
187+
@Override
188+
public void close() {
189+
}
190+
191+
}
192+
119193
}

spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
6565

6666
private HttpClient httpClient;
6767

68-
private boolean bufferRequestBody = true;
69-
7068
@Nullable
7169
private BiFunction<HttpMethod, URI, HttpContext> httpContextFactory;
7270

@@ -146,9 +144,11 @@ public void setConnectionRequestTimeout(int connectionRequestTimeout) {
146144
* <p>Default is {@code true}. When sending large amounts of data via POST or PUT, it is
147145
* recommended to change this property to {@code false}, so as not to run out of memory.
148146
* @since 4.0
147+
* @deprecated since 6.1 requests are never buffered, as if this property is {@code false}
149148
*/
149+
@Deprecated(since = "6.1", forRemoval = true)
150150
public void setBufferRequestBody(boolean bufferRequestBody) {
151-
this.bufferRequestBody = bufferRequestBody;
151+
// no-op
152152
}
153153

154154
/**
@@ -190,13 +190,7 @@ public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IO
190190
context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
191191
}
192192
}
193-
194-
if (this.bufferRequestBody) {
195-
return new HttpComponentsClientHttpRequest(client, httpRequest, context);
196-
}
197-
else {
198-
return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, context);
199-
}
193+
return new HttpComponentsClientHttpRequest(client, httpRequest, context);
200194
}
201195

202196

0 commit comments

Comments
 (0)