24
24
import java .net .http .HttpClient ;
25
25
import java .net .http .HttpRequest ;
26
26
import java .net .http .HttpResponse ;
27
+ import java .net .http .HttpResponse .BodyHandler ;
28
+ import java .net .http .HttpResponse .BodySubscriber ;
29
+ import java .net .http .HttpResponse .BodySubscribers ;
30
+ import java .net .http .HttpResponse .ResponseInfo ;
27
31
import java .net .http .HttpTimeoutException ;
28
32
import java .nio .ByteBuffer ;
29
33
import java .time .Duration ;
30
34
import java .util .Collections ;
35
+ import java .util .List ;
31
36
import java .util .Locale ;
32
37
import java .util .Set ;
33
38
import java .util .TreeSet ;
38
43
import java .util .concurrent .Flow ;
39
44
import java .util .concurrent .TimeUnit ;
40
45
import java .util .concurrent .atomic .AtomicBoolean ;
46
+ import java .util .zip .GZIPInputStream ;
47
+ import java .util .zip .InflaterInputStream ;
41
48
42
49
import org .jspecify .annotations .Nullable ;
43
50
@@ -60,6 +67,8 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
60
67
61
68
private static final Set <String > DISALLOWED_HEADERS = disallowedHeaders ();
62
69
70
+ private static final List <String > ALLOWED_ENCODINGS = List .of ("gzip" , "deflate" );
71
+
63
72
64
73
private final HttpClient httpClient ;
65
74
@@ -71,15 +80,18 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
71
80
72
81
private final @ Nullable Duration timeout ;
73
82
83
+ private final boolean compressionEnabled ;
84
+
74
85
75
86
public JdkClientHttpRequest (HttpClient httpClient , URI uri , HttpMethod method , Executor executor ,
76
- @ Nullable Duration readTimeout ) {
87
+ @ Nullable Duration readTimeout , boolean compressionEnabled ) {
77
88
78
89
this .httpClient = httpClient ;
79
90
this .uri = uri ;
80
91
this .method = method ;
81
92
this .executor = executor ;
82
93
this .timeout = readTimeout ;
94
+ this .compressionEnabled = compressionEnabled ;
83
95
}
84
96
85
97
@@ -100,7 +112,7 @@ protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body
100
112
TimeoutHandler timeoutHandler = null ;
101
113
try {
102
114
HttpRequest request = buildRequest (headers , body );
103
- responseFuture = this .httpClient .sendAsync (request , HttpResponse . BodyHandlers . ofInputStream ());
115
+ responseFuture = this .httpClient .sendAsync (request , new DecompressingBodyHandler ());
104
116
105
117
if (this .timeout != null ) {
106
118
timeoutHandler = new TimeoutHandler (responseFuture , this .timeout );
@@ -152,6 +164,15 @@ else if (cause instanceof IOException ioEx) {
152
164
private HttpRequest buildRequest (HttpHeaders headers , @ Nullable Body body ) {
153
165
HttpRequest .Builder builder = HttpRequest .newBuilder ().uri (this .uri );
154
166
167
+ // When compression is enabled and valid encoding is absent, we add gzip as standard encoding
168
+ if (this .compressionEnabled ) {
169
+ if (headers .containsHeader (HttpHeaders .ACCEPT_ENCODING ) &&
170
+ !ALLOWED_ENCODINGS .contains (headers .getFirst (HttpHeaders .ACCEPT_ENCODING ))) {
171
+ headers .remove (HttpHeaders .ACCEPT_ENCODING );
172
+ }
173
+ headers .add (HttpHeaders .ACCEPT_ENCODING , "gzip" );
174
+ }
175
+
155
176
headers .forEach ((headerName , headerValues ) -> {
156
177
if (!DISALLOWED_HEADERS .contains (headerName .toLowerCase (Locale .ROOT ))) {
157
178
for (String headerValue : headerValues ) {
@@ -237,7 +258,7 @@ public ByteBuffer map(byte[] b, int off, int len) {
237
258
/**
238
259
* Temporary workaround to use instead of {@link HttpRequest.Builder#timeout(Duration)}
239
260
* until <a href="https://bugs.openjdk.org/browse/JDK-8258397">JDK-8258397</a>
240
- * is fixed. Essentially, create a future wiht a timeout handler, and use it
261
+ * is fixed. Essentially, create a future with a timeout handler, and use it
241
262
* to close the response.
242
263
* @see <a href="https://mail.openjdk.org/pipermail/net-dev/2021-October/016672.html">OpenJDK discussion thread</a>
243
264
*/
@@ -288,4 +309,39 @@ public void handleCancellationException(CancellationException ex) throws HttpTim
288
309
}
289
310
}
290
311
312
+ /**
313
+ * Custom BodyHandler that checks the Content-Encoding header and applies the appropriate decompression algorithm.
314
+ * Supports Gzip and Deflate encoded responses.
315
+ */
316
+ public static final class DecompressingBodyHandler implements BodyHandler <InputStream > {
317
+
318
+ @ Override
319
+ public BodySubscriber <InputStream > apply (ResponseInfo responseInfo ) {
320
+ String contentEncoding = responseInfo .headers ().firstValue (HttpHeaders .CONTENT_ENCODING ).orElse ("" );
321
+ if (contentEncoding .equalsIgnoreCase ("gzip" )) {
322
+ // If the content is gzipped, wrap the InputStream with a GZIPInputStream
323
+ return BodySubscribers .mapping (
324
+ BodySubscribers .ofInputStream (),
325
+ (InputStream is ) -> {
326
+ try {
327
+ return new GZIPInputStream (is );
328
+ }
329
+ catch (IOException ex ) {
330
+ throw new UncheckedIOException (ex ); // Propagate IOExceptions
331
+ }
332
+ });
333
+ }
334
+ else if (contentEncoding .equalsIgnoreCase ("deflate" )) {
335
+ // If the content is encoded using deflate, wrap the InputStream with a InflaterInputStream
336
+ return BodySubscribers .mapping (
337
+ BodySubscribers .ofInputStream (),
338
+ InflaterInputStream ::new );
339
+ }
340
+ else {
341
+ // Otherwise, return a standard InputStream BodySubscriber
342
+ return BodySubscribers .ofInputStream ();
343
+ }
344
+ }
345
+ }
346
+
291
347
}
0 commit comments