Skip to content

Commit b4e19ea

Browse files
committed
Added support for deflate encoding and test cases
1 parent 3015e29 commit b4e19ea

File tree

3 files changed

+90
-12
lines changed

3 files changed

+90
-12
lines changed

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

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
import java.net.http.HttpClient;
2525
import java.net.http.HttpRequest;
2626
import java.net.http.HttpResponse;
27-
import java.net.http.HttpTimeoutException;
2827
import java.net.http.HttpResponse.BodyHandler;
2928
import java.net.http.HttpResponse.BodySubscriber;
3029
import java.net.http.HttpResponse.BodySubscribers;
3130
import java.net.http.HttpResponse.ResponseInfo;
31+
import java.net.http.HttpTimeoutException;
3232
import java.nio.ByteBuffer;
3333
import java.time.Duration;
3434
import java.util.Collections;
35+
import java.util.List;
3536
import java.util.Locale;
3637
import java.util.Set;
3738
import java.util.TreeSet;
@@ -42,6 +43,7 @@
4243
import java.util.concurrent.Flow;
4344
import java.util.concurrent.TimeUnit;
4445
import java.util.zip.GZIPInputStream;
46+
import java.util.zip.InflaterInputStream;
4547

4648
import org.jspecify.annotations.Nullable;
4749

@@ -64,6 +66,8 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
6466

6567
private static final Set<String> DISALLOWED_HEADERS = disallowedHeaders();
6668

69+
private static final List<String> ALLOWED_ENCODINGS = List.of("gzip", "deflate");
70+
6771

6872
private final HttpClient httpClient;
6973

@@ -106,11 +110,7 @@ protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body
106110
CompletableFuture<HttpResponse<InputStream>> responseFuture = null;
107111
try {
108112
HttpRequest request = buildRequest(headers, body);
109-
if (compressionEnabled) {
110-
responseFuture = this.httpClient.sendAsync(request, new DecompressingBodyHandler());
111-
} else {
112-
responseFuture = this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream());
113-
}
113+
responseFuture = this.httpClient.sendAsync(request, new DecompressingBodyHandler());
114114

115115
if (this.timeout != null) {
116116
TimeoutHandler timeoutHandler = new TimeoutHandler(responseFuture, this.timeout);
@@ -152,8 +152,13 @@ else if (cause instanceof IOException ioEx) {
152152

153153
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
154154
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(this.uri);
155-
156-
if (compressionEnabled) {
155+
156+
// When compression is enabled and valid encoding is absent, we add gzip as standard encoding
157+
if (this.compressionEnabled) {
158+
if (headers.containsHeader(HttpHeaders.ACCEPT_ENCODING) &&
159+
!ALLOWED_ENCODINGS.contains(headers.getFirst(HttpHeaders.ACCEPT_ENCODING))) {
160+
headers.remove(HttpHeaders.ACCEPT_ENCODING);
161+
}
157162
headers.add(HttpHeaders.ACCEPT_ENCODING, "gzip");
158163
}
159164

@@ -242,7 +247,7 @@ public ByteBuffer map(byte[] b, int off, int len) {
242247
/**
243248
* Temporary workaround to use instead of {@link HttpRequest.Builder#timeout(Duration)}
244249
* until <a href="https://bugs.openjdk.org/browse/JDK-8258397">JDK-8258397</a>
245-
* is fixed. Essentially, create a future wiht a timeout handler, and use it
250+
* is fixed. Essentially, create a future with a timeout handler, and use it
246251
* to close the response.
247252
* @see <a href="https://mail.openjdk.org/pipermail/net-dev/2021-October/016672.html">OpenJDK discussion thread</a>
248253
*/
@@ -287,6 +292,7 @@ public void close() throws IOException {
287292

288293
/**
289294
* Custom BodyHandler that checks the Content-Encoding header and applies the appropriate decompression algorithm.
295+
* Supports Gzip and Deflate encoded responses.
290296
*/
291297
public static final class DecompressingBodyHandler implements BodyHandler<InputStream> {
292298

@@ -300,11 +306,19 @@ public BodySubscriber<InputStream> apply(ResponseInfo responseInfo) {
300306
(InputStream is) -> {
301307
try {
302308
return new GZIPInputStream(is);
303-
} catch (IOException e) {
304-
throw new UncheckedIOException(e); // Propagate IOExceptions
309+
}
310+
catch (IOException ex) {
311+
throw new UncheckedIOException(ex); // Propagate IOExceptions
305312
}
306313
});
307-
} else {
314+
}
315+
else if (contentEncoding.equalsIgnoreCase("deflate")) {
316+
// If the content is encoded using deflate, wrap the InputStream with a InflaterInputStream
317+
return BodySubscribers.mapping(
318+
BodySubscribers.ofInputStream(),
319+
InflaterInputStream::new);
320+
}
321+
else {
308322
// Otherwise, return a standard InputStream BodySubscriber
309323
return BodySubscribers.ofInputStream();
310324
}

spring-web/src/test/java/org/springframework/http/client/AbstractMockWebServerTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@
2323
import org.junit.jupiter.api.AfterEach;
2424
import org.junit.jupiter.api.BeforeEach;
2525

26+
import org.springframework.http.HttpHeaders;
2627
import org.springframework.util.StringUtils;
2728

29+
import java.io.ByteArrayOutputStream;
30+
import java.nio.charset.StandardCharsets;
31+
import java.util.zip.DeflaterOutputStream;
32+
import java.util.zip.GZIPOutputStream;
33+
2834
import static org.assertj.core.api.Assertions.assertThat;
2935

3036
/**
@@ -106,6 +112,26 @@ else if(request.getTarget().startsWith("/header/")) {
106112
String headerName = request.getTarget().replace("/header/","");
107113
return new MockResponse.Builder().body(headerName + ":" + request.getHeaders().get(headerName)).code(200).build();
108114
}
115+
else if(request.getTarget().startsWith("/compress/")) {
116+
String encoding = request.getTarget().replace("/compress/","");
117+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
118+
if (encoding.equals("gzip")) {
119+
try(GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
120+
gzipOutputStream.write("Test Payload".getBytes());
121+
gzipOutputStream.flush();
122+
}
123+
}
124+
else if(encoding.equals("deflate")) {
125+
try(DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
126+
deflaterOutputStream.write("Test Payload".getBytes());
127+
deflaterOutputStream.flush();
128+
}
129+
} else {
130+
byteArrayOutputStream.write("Test Payload".getBytes());
131+
}
132+
return new MockResponse.Builder().body(byteArrayOutputStream.toString(StandardCharsets.ISO_8859_1))
133+
.code(200).setHeader(HttpHeaders.CONTENT_ENCODING, encoding).build();
134+
}
109135
return new MockResponse.Builder().code(404).build();
110136
}
111137
catch (Throwable ex) {

spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestFactoryTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,44 @@ void deleteRequestWithBody() throws Exception {
108108
}
109109
}
110110

111+
@Test
112+
void compressionDisabled() throws IOException {
113+
URI uri = URI.create(baseUrl + "/compress/");
114+
ClientHttpRequest request = this.factory.createRequest(uri, HttpMethod.GET);
115+
try (ClientHttpResponse response = request.execute()) {
116+
assertThat(response.getStatusCode()).as("Invalid response status").isEqualTo(HttpStatus.OK);
117+
assertThat(StreamUtils.copyToString(response.getBody(), StandardCharsets.ISO_8859_1))
118+
.as("Invalid request body").isEqualTo("Test Payload");
119+
}
120+
}
121+
122+
@Test
123+
void compressionGzip() throws IOException {
124+
URI uri = URI.create(baseUrl + "/compress/gzip");
125+
JdkClientHttpRequestFactory requestFactory = (JdkClientHttpRequestFactory) this.factory;
126+
requestFactory.setCompressionEnabled(true);
127+
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET);
128+
129+
try (ClientHttpResponse response = request.execute()) {
130+
assertThat(response.getStatusCode()).as("Invalid response status").isEqualTo(HttpStatus.OK);
131+
assertThat(StreamUtils.copyToString(response.getBody(), StandardCharsets.ISO_8859_1))
132+
.as("Invalid request body").isEqualTo("Test Payload");
133+
}
134+
}
135+
136+
@Test
137+
void compressionDeflate() throws IOException {
138+
URI uri = URI.create(baseUrl + "/compress/deflate");
139+
JdkClientHttpRequestFactory requestFactory = (JdkClientHttpRequestFactory) this.factory;
140+
requestFactory.setCompressionEnabled(true);
141+
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET);
142+
try (ClientHttpResponse response = request.execute()) {
143+
assertThat(response.getStatusCode()).as("Invalid response status").isEqualTo(HttpStatus.OK);
144+
assertThat(StreamUtils.copyToString(response.getBody(), StandardCharsets.ISO_8859_1))
145+
.as("Invalid request body").isEqualTo("Test Payload");
146+
}
147+
}
148+
111149
@Test // gh-34971
112150
@EnabledForJreRange(min = JRE.JAVA_19) // behavior fixed in Java 19
113151
void requestContentLengthHeaderWhenNoBody() throws Exception {

0 commit comments

Comments
 (0)