diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingAsyncDataConsumer.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingAsyncDataConsumer.java index 76a846877d..1dad994655 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingAsyncDataConsumer.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingAsyncDataConsumer.java @@ -36,6 +36,7 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.nio.AsyncDataConsumer; +import org.apache.hc.core5.util.Args; import org.apache.hc.core5.http.nio.CapacityChannel; /** @@ -65,7 +66,7 @@ public final class InflatingAsyncDataConsumer implements AsyncDataConsumer { public InflatingAsyncDataConsumer( final AsyncDataConsumer downstream, final Boolean nowrapHint) { - this.downstream = downstream; + this.downstream = Args.notNull(downstream, "Downstream data consumer"); this.nowrapHint = nowrapHint; this.inflater = new Inflater(nowrapHint == null || nowrapHint); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumer.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumer.java index 462264bbee..52e8f07c27 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumer.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingBrotliDataConsumer.java @@ -35,6 +35,7 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.nio.AsyncDataConsumer; +import org.apache.hc.core5.util.Args; import org.apache.hc.core5.http.nio.CapacityChannel; import org.apache.hc.core5.util.Asserts; @@ -70,7 +71,7 @@ public final class InflatingBrotliDataConsumer implements AsyncDataConsumer { public InflatingBrotliDataConsumer(final AsyncDataConsumer downstream) { - this.downstream = downstream; + this.downstream = Args.notNull(downstream, "Downstream data consumer"); try { this.decoder = new DecoderJNI.Wrapper(8 * 1024); } catch (final IOException e) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java index ce7b97908e..27452a596c 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java @@ -39,6 +39,7 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.nio.AsyncDataConsumer; +import org.apache.hc.core5.util.Args; import org.apache.hc.core5.http.nio.CapacityChannel; /** @@ -65,7 +66,7 @@ public final class InflatingGzipDataConsumer implements AsyncDataConsumer { private final AtomicBoolean closed = new AtomicBoolean(false); public InflatingGzipDataConsumer(final AsyncDataConsumer downstream) { - this.downstream = downstream; + this.downstream = Args.notNull(downstream, "Downstream data consumer"); } @Override diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingZstdDataConsumer.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingZstdDataConsumer.java index 126e9a17d1..3e633bfe96 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingZstdDataConsumer.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingZstdDataConsumer.java @@ -36,6 +36,7 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.nio.AsyncDataConsumer; +import org.apache.hc.core5.util.Args; import org.apache.hc.core5.http.nio.CapacityChannel; /** @@ -73,7 +74,7 @@ public final class InflatingZstdDataConsumer implements AsyncDataConsumer { private final AtomicBoolean closed = new AtomicBoolean(false); public InflatingZstdDataConsumer(final AsyncDataConsumer downstream) { - this.downstream = downstream; + this.downstream = Args.notNull(downstream, "Downstream data consumer"); inDirect.limit(0); outDirect.limit(0); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java index d685b39cd2..cf29083429 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java @@ -152,6 +152,9 @@ public AsyncDataConsumer handleResponse(final HttpResponse rsp, ContentCodingSupport.validate(codecs, maxCodecListLen); if (!codecs.isEmpty()) { AsyncDataConsumer downstream = cb.handleResponse(rsp, wrapEntityDetails(details)); + if (downstream == null) { + return null; + } for (int i = codecs.size() - 1; i >= 0; i--) { final String codec = codecs.get(i); final UnaryOperator op = decoders.lookup(codec); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestInflatingGzipDataConsumerNullDownstream.java b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestInflatingGzipDataConsumerNullDownstream.java new file mode 100644 index 0000000000..4eb2170abb --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/TestInflatingGzipDataConsumerNullDownstream.java @@ -0,0 +1,39 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.async.methods; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; +class TestInflatingGzipDataConsumerNullDownstream { + + @Test + void gzipConsumerRejectsNullDownstreamAtConstruction() { + assertThrows(NullPointerException.class, () -> new InflatingGzipDataConsumer(null)); + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java index 3d93605716..be055709a4 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestContentCompressionAsyncExec.java @@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -133,7 +134,8 @@ void testDeflateConsumerInserted() throws Exception { when(details.getContentEncoding()).thenReturn("deflate"); final AsyncDataConsumer downstream = new StringAsyncEntityConsumer(); - when(originalCb.handleResponse(same(rsp), same(details))).thenReturn(downstream); + // the exec passes a wrapped EntityDetails downstream, so match any (not same(details)) + when(originalCb.handleResponse(same(rsp), any(EntityDetails.class))).thenReturn(downstream); final AsyncDataConsumer wrapped = cb.handleResponse(rsp, details); @@ -175,6 +177,10 @@ public AsyncDataConsumer apply(final AsyncDataConsumer d) { final HttpResponse rsp = new BasicHttpResponse(200, "OK"); final EntityDetails details = mock(EntityDetails.class); when(details.getContentEncoding()).thenReturn("whatever"); + // a real (non-null) downstream so the exec proceeds to decoder selection and rejects the + // unknown coding; a null downstream would legitimately be discarded without decoding + when(originalCb.handleResponse(same(rsp), any(EntityDetails.class))) + .thenReturn(new StringAsyncEntityConsumer()); assertThrows(HttpException.class, () -> cb.handleResponse(rsp, details)); } @@ -200,7 +206,7 @@ void testContentEncodingExceedsCodecListLenMax() throws Exception { when(details1.getContentEncoding()).thenReturn("gzip,gzip,gzip,gzip,gzip"); final AsyncDataConsumer downstream1 = new StringAsyncEntityConsumer(); - when(originalCb.handleResponse(same(rsp1), same(details1))).thenReturn(downstream1); + when(originalCb.handleResponse(same(rsp1), any(EntityDetails.class))).thenReturn(downstream1); final AsyncDataConsumer wrapped = cb.handleResponse(rsp1, details1); @@ -217,4 +223,20 @@ void testContentEncodingExceedsCodecListLenMax() throws Exception { assertEquals("Codec list exceeds maximum of 5 elements", exception.getMessage()); } + @Test + void propagatesNullDiscardOnEncodedResponse() throws Exception { + final HttpRequest request = new BasicHttpRequest(Method.GET, "/"); + final AsyncExecCallback cb = executeAndCapture(request); + + final HttpResponse rsp = new BasicHttpResponse(302, "Found"); + final EntityDetails details = mock(EntityDetails.class); + when(details.getContentEncoding()).thenReturn("gzip"); + + // an upstream exec (e.g. AsyncRedirectExec on a redirect) discards the body by returning null + when(originalCb.handleResponse(same(rsp), any(EntityDetails.class))).thenReturn(null); + + // the null-discard signal must be propagated, not wrapped in a decoder (HTTPCLIENT-2426) + assertNull(cb.handleResponse(rsp, details)); + } + } \ No newline at end of file