Skip to content

Commit ab20c86

Browse files
committed
HTTPCLIENT-2426: Do not wrap a discarded (null) response body in a content decoder
(cherry picked from commit 06aac6d)
1 parent f092f56 commit ab20c86

7 files changed

Lines changed: 74 additions & 6 deletions

File tree

httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingAsyncDataConsumer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.apache.hc.core5.http.Header;
3737
import org.apache.hc.core5.http.HttpException;
3838
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
39+
import org.apache.hc.core5.util.Args;
3940
import org.apache.hc.core5.http.nio.CapacityChannel;
4041

4142
/**
@@ -65,7 +66,7 @@ public final class InflatingAsyncDataConsumer implements AsyncDataConsumer {
6566

6667
public InflatingAsyncDataConsumer(
6768
final AsyncDataConsumer downstream, final Boolean nowrapHint) {
68-
this.downstream = downstream;
69+
this.downstream = Args.notNull(downstream, "Downstream data consumer");
6970
this.nowrapHint = nowrapHint;
7071
this.inflater = new Inflater(nowrapHint == null || nowrapHint);
7172
}

httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.hc.core5.http.Header;
3636
import org.apache.hc.core5.http.HttpException;
3737
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
38+
import org.apache.hc.core5.util.Args;
3839
import org.apache.hc.core5.http.nio.CapacityChannel;
3940
import org.apache.hc.core5.util.Asserts;
4041

@@ -70,7 +71,7 @@ public final class InflatingBrotliDataConsumer implements AsyncDataConsumer {
7071

7172

7273
public InflatingBrotliDataConsumer(final AsyncDataConsumer downstream) {
73-
this.downstream = downstream;
74+
this.downstream = Args.notNull(downstream, "Downstream data consumer");
7475
try {
7576
this.decoder = new DecoderJNI.Wrapper(8 * 1024);
7677
} catch (final IOException e) {

httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.hc.core5.http.Header;
4040
import org.apache.hc.core5.http.HttpException;
4141
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
42+
import org.apache.hc.core5.util.Args;
4243
import org.apache.hc.core5.http.nio.CapacityChannel;
4344

4445
/**
@@ -65,7 +66,7 @@ public final class InflatingGzipDataConsumer implements AsyncDataConsumer {
6566
private final AtomicBoolean closed = new AtomicBoolean(false);
6667

6768
public InflatingGzipDataConsumer(final AsyncDataConsumer downstream) {
68-
this.downstream = downstream;
69+
this.downstream = Args.notNull(downstream, "Downstream data consumer");
6970
}
7071

7172
@Override

httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingZstdDataConsumer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.apache.hc.core5.http.Header;
3737
import org.apache.hc.core5.http.HttpException;
3838
import org.apache.hc.core5.http.nio.AsyncDataConsumer;
39+
import org.apache.hc.core5.util.Args;
3940
import org.apache.hc.core5.http.nio.CapacityChannel;
4041

4142
/**
@@ -73,7 +74,7 @@ public final class InflatingZstdDataConsumer implements AsyncDataConsumer {
7374
private final AtomicBoolean closed = new AtomicBoolean(false);
7475

7576
public InflatingZstdDataConsumer(final AsyncDataConsumer downstream) {
76-
this.downstream = downstream;
77+
this.downstream = Args.notNull(downstream, "Downstream data consumer");
7778
inDirect.limit(0);
7879
outDirect.limit(0);
7980
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ public AsyncDataConsumer handleResponse(final HttpResponse rsp,
152152
ContentCodingSupport.validate(codecs, maxCodecListLen);
153153
if (!codecs.isEmpty()) {
154154
AsyncDataConsumer downstream = cb.handleResponse(rsp, wrapEntityDetails(details));
155+
if (downstream == null) {
156+
return null;
157+
}
155158
for (int i = codecs.size() - 1; i >= 0; i--) {
156159
final String codec = codecs.get(i);
157160
final UnaryOperator<AsyncDataConsumer> op = decoders.lookup(codec);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.async.methods;
28+
29+
import static org.junit.jupiter.api.Assertions.assertThrows;
30+
31+
import org.junit.jupiter.api.Test;
32+
class TestInflatingGzipDataConsumerNullDownstream {
33+
34+
@Test
35+
void gzipConsumerRejectsNullDownstreamAtConstruction() {
36+
assertThrows(NullPointerException.class, () -> new InflatingGzipDataConsumer(null));
37+
}
38+
39+
}

httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static org.junit.jupiter.api.Assertions.assertEquals;
3030
import static org.junit.jupiter.api.Assertions.assertFalse;
3131
import static org.junit.jupiter.api.Assertions.assertNotNull;
32+
import static org.junit.jupiter.api.Assertions.assertNull;
3233
import static org.junit.jupiter.api.Assertions.assertSame;
3334
import static org.junit.jupiter.api.Assertions.assertThrows;
3435
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -130,7 +131,8 @@ void testDeflateConsumerInserted() throws Exception {
130131
when(details.getContentEncoding()).thenReturn("deflate");
131132

132133
final AsyncDataConsumer downstream = new StringAsyncEntityConsumer();
133-
when(originalCb.handleResponse(same(rsp), same(details))).thenReturn(downstream);
134+
// the exec passes a wrapped EntityDetails downstream, so match any (not same(details))
135+
when(originalCb.handleResponse(same(rsp), any(EntityDetails.class))).thenReturn(downstream);
134136

135137
final AsyncDataConsumer wrapped = cb.handleResponse(rsp, details);
136138

@@ -172,6 +174,10 @@ public AsyncDataConsumer apply(final AsyncDataConsumer d) {
172174
final HttpResponse rsp = new BasicHttpResponse(200, "OK");
173175
final EntityDetails details = mock(EntityDetails.class);
174176
when(details.getContentEncoding()).thenReturn("whatever");
177+
// a real (non-null) downstream so the exec proceeds to decoder selection and rejects the
178+
// unknown coding; a null downstream would legitimately be discarded without decoding
179+
when(originalCb.handleResponse(same(rsp), any(EntityDetails.class)))
180+
.thenReturn(new StringAsyncEntityConsumer());
175181

176182
assertThrows(HttpException.class, () -> cb.handleResponse(rsp, details));
177183
}
@@ -197,7 +203,7 @@ void testContentEncodingExceedsCodecListLenMax() throws Exception {
197203
when(details1.getContentEncoding()).thenReturn("gzip,gzip,gzip,gzip,gzip");
198204

199205
final AsyncDataConsumer downstream1 = new StringAsyncEntityConsumer();
200-
when(originalCb.handleResponse(same(rsp1), same(details1))).thenReturn(downstream1);
206+
when(originalCb.handleResponse(same(rsp1), any(EntityDetails.class))).thenReturn(downstream1);
201207

202208
final AsyncDataConsumer wrapped = cb.handleResponse(rsp1, details1);
203209

@@ -214,4 +220,20 @@ void testContentEncodingExceedsCodecListLenMax() throws Exception {
214220
assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage());
215221
}
216222

223+
@Test
224+
void propagatesNullDiscardOnEncodedResponse() throws Exception {
225+
final HttpRequest request = new BasicHttpRequest(Method.GET, "/");
226+
final AsyncExecCallback cb = executeAndCapture(request);
227+
228+
final HttpResponse rsp = new BasicHttpResponse(302, "Found");
229+
final EntityDetails details = mock(EntityDetails.class);
230+
when(details.getContentEncoding()).thenReturn("gzip");
231+
232+
// an upstream exec (e.g. AsyncRedirectExec on a redirect) discards the body by returning null
233+
when(originalCb.handleResponse(same(rsp), any(EntityDetails.class))).thenReturn(null);
234+
235+
// the null-discard signal must be propagated, not wrapped in a decoder (HTTPCLIENT-2426)
236+
assertNull(cb.handleResponse(rsp, details));
237+
}
238+
217239
}

0 commit comments

Comments
 (0)