From c01ee0bc4bbba577613e6c81f920283d05476983 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 5 Aug 2025 15:02:43 +0200 Subject: [PATCH] Transparent async content (de)compression with gzip --- .../methods/DeflatingGzipEntityProducer.java | 250 ++++++++++++++++++ .../methods/InflatingGzipDataConsumer.java | 157 +++++++++++ .../async/ContentCompressionAsyncExec.java | 11 +- .../http/async/methods/GzipRoundTripTest.java | 172 ++++++++++++ .../AsyncClientGzipCompressionExample.java | 77 ++++++ .../AsyncClientGzipDecompressionExample.java | 99 +++++++ .../TestContentCompressionAsyncExec.java | 2 +- 7 files changed, 764 insertions(+), 4 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/DeflatingGzipEntityProducer.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/GzipRoundTripTest.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipCompressionExample.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipDecompressionExample.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/DeflatingGzipEntityProducer.java b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/DeflatingGzipEntityProducer.java new file mode 100644 index 0000000000..5f6ccf993d --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/DeflatingGzipEntityProducer.java @@ -0,0 +1,250 @@ +/* + * ==================================================================== + * 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.util.Args; + +/** + * Streams an {@link AsyncEntityProducer} through raw DEFLATE + * and wraps the result in a valid GZIP container. + *

+ * Memory usage is bounded (8 KiB buffers) and back-pressure + * from the I/O reactor is honoured. + * + * @since 5.6 + */ +public final class DeflatingGzipEntityProducer implements AsyncEntityProducer { + + /* ---------------- constants & buffers --------------------------- */ + + private static final int IN_BUF = 8 * 1024; + private static final int OUT_BUF = 8 * 1024; + + private final AsyncEntityProducer delegate; + private final CRC32 crc = new CRC32(); + private final Deflater deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + private final byte[] in = new byte[IN_BUF]; + private final ByteBuffer outBuf = ByteBuffer.allocate(OUT_BUF); + + private boolean headerSent = false; + private boolean finished = false; + private long uncompressed = 0; + + private final AtomicBoolean released = new AtomicBoolean(false); + + public DeflatingGzipEntityProducer(final AsyncEntityProducer delegate) { + this.delegate = Args.notNull(delegate, "delegate"); + outBuf.flip(); // start in “read mode” with no data + } + + /* ------------------- metadata ------------------- */ + + @Override + public boolean isRepeatable() { + return delegate.isRepeatable(); + } + + @Override + public long getContentLength() { + return -1; + } // unknown + + @Override + public String getContentType() { + return delegate.getContentType(); + } + + @Override + public String getContentEncoding() { + return "gzip"; + } + + @Override + public boolean isChunked() { + return true; + } + + @Override + public Set getTrailerNames() { + return Collections.emptySet(); + } + + @Override + public int available() { + return outBuf.hasRemaining() ? outBuf.remaining() : delegate.available(); + } + + /* ------------------- core ----------------------- */ + + @Override + public void produce(final DataStreamChannel chan) throws IOException { + + flushOut(chan); // 1) flush any pending data + + if (finished) { + return; // already done + } + + delegate.produce(new InnerChannel(chan)); // 2) pull more input + + /* 3) when delegate is done → finish deflater, drain, trailer */ + if (delegate.available() == 0 && !finished) { + + deflater.finish(); // signal EOF to compressor + while (!deflater.finished()) { // drain *everything* + deflateToOut(); + flushOut(chan); + } + + /* ---------------- little-endian trailer ---------------- */ + final int crcVal = (int) crc.getValue(); + final int size = (int) uncompressed; + + final byte[] trailer = { + (byte) crcVal, (byte) (crcVal >>> 8), + (byte) (crcVal >>> 16), (byte) (crcVal >>> 24), + (byte) size, (byte) (size >>> 8), + (byte) (size >>> 16), (byte) (size >>> 24) + }; + chan.write(ByteBuffer.wrap(trailer)); + + finished = true; + chan.endStream(); + } + } + + /* copy all currently available bytes from deflater into outBuf */ + private void deflateToOut() { + outBuf.compact(); // switch to “write mode” + byte[] arr = outBuf.array(); + int pos = outBuf.position(); + int lim = outBuf.limit(); + int n; + while ((n = deflater.deflate(arr, pos, lim - pos, Deflater.NO_FLUSH)) > 0) { + pos += n; + if (pos == lim) { // buffer full → grow 2× + final ByteBuffer bigger = ByteBuffer.allocate(arr.length * 2); + outBuf.flip(); + bigger.put(outBuf); + outBuf.clear(); + outBuf.put(bigger); + arr = outBuf.array(); + lim = outBuf.limit(); + pos = outBuf.position(); + } + } + outBuf.position(pos); + outBuf.flip(); // back to “read mode” + } + + /* send as much of outBuf as the channel will accept */ + private void flushOut(final DataStreamChannel chan) throws IOException { + while (outBuf.hasRemaining()) { + final int written = chan.write(outBuf); + if (written == 0) { + break; // back-pressure + } + } + } + + /* --------------- inner channel feeding deflater ---------------- */ + + private final class InnerChannel implements DataStreamChannel { + private final DataStreamChannel chan; + + InnerChannel(final DataStreamChannel chan) { + this.chan = chan; + } + + @Override + public void requestOutput() { + chan.requestOutput(); + } + + @Override + public int write(final ByteBuffer src) throws IOException { + + if (!headerSent) { // write 10-byte GZIP header + chan.write(ByteBuffer.wrap(new byte[]{ + 0x1f, (byte) 0x8b, 8, 0, 0, 0, 0, 0, 0, 0 + })); + headerSent = true; + } + + int consumed = 0; + while (src.hasRemaining()) { + final int chunk = Math.min(src.remaining(), in.length); + src.get(in, 0, chunk); + + crc.update(in, 0, chunk); + uncompressed += chunk; + + deflater.setInput(in, 0, chunk); + consumed += chunk; + + deflateToOut(); + flushOut(chan); + } + return consumed; + } + + @Override + public void endStream() { /* delegate.available()==0 is our signal */ } + + @Override + public void endStream(final List t) { + endStream(); + } + } + + /* ---------------- failure / cleanup ---------------------------- */ + + @Override + public void failed(final Exception cause) { + delegate.failed(cause); + } + + @Override + public void releaseResources() { + if (released.compareAndSet(false, true)) { + deflater.end(); + delegate.releaseResources(); + } + } +} 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 new file mode 100644 index 0000000000..ce7b97908e --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/async/methods/InflatingGzipDataConsumer.java @@ -0,0 +1,157 @@ +/* + * ==================================================================== + * 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.CRC32; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +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.http.nio.CapacityChannel; + +/** + * Streaming {@link AsyncDataConsumer} that inflates {@code Content-Encoding: + * gzip}. It parses the GZIP header on the fly, passes the deflated body + * through a {@link java.util.zip.Inflater} and verifies CRC + length trailer. + * + *

The implementation is fully non-blocking and honours back-pressure.

+ * + * @since 5.6 + */ +public final class InflatingGzipDataConsumer implements AsyncDataConsumer { + + private static final int OUT = 8 * 1024; + + private final AsyncDataConsumer downstream; + private final Inflater inflater = new Inflater(true); // raw DEFLATE + private final CRC32 crc = new CRC32(); + + private final byte[] out = new byte[OUT]; + private final ByteArrayOutputStream headerBuf = new ByteArrayOutputStream(18); + + private boolean headerDone = false; + private final AtomicBoolean closed = new AtomicBoolean(false); + + public InflatingGzipDataConsumer(final AsyncDataConsumer downstream) { + this.downstream = downstream; + } + + @Override + public void updateCapacity(final CapacityChannel c) throws IOException { + downstream.updateCapacity(c); + } + + @Override + public void consume(final ByteBuffer src) throws IOException { + if (closed.get()) return; + + /* ----------- parse GZIP header first ------------------------ */ + if (!headerDone) { + while (src.hasRemaining() && headerBuf.size() < 10) { + headerBuf.write(src.get()); + } + if (headerBuf.size() < 10) { + return; // need more + } + + final byte[] hdr = headerBuf.toByteArray(); + if (hdr[0] != 0x1f || hdr[1] != (byte) 0x8b || hdr[2] != 8) { + throw new IOException("Malformed GZIP header"); + } + int flg = hdr[3] & 0xff; + + int need = 10; + if ((flg & 0x04) != 0) { + need += 2; // extra len (will read later) + } + if ((flg & 0x08) != 0) { + need = Integer.MAX_VALUE; // fname – scan to 0 + } + if ((flg & 0x10) != 0) { + need = Integer.MAX_VALUE; // fcomment – scan to 0 + } + if ((flg & 0x02) != 0) { + need += 2; // header CRC + } + + while (src.hasRemaining() && headerBuf.size() < need) { + headerBuf.write(src.get()); + if (need == Integer.MAX_VALUE && headerBuf.toByteArray()[headerBuf.size() - 1] == 0) { + // zero-terminated section finished; keep reading until flags handled + if (flg == 0x08 || flg == 0x10) { + flg ^= flg & 0x18; // clear fname/fcomment flag + } + if ((flg & 0x18) == 0) { + need = headerBuf.size(); // done + } + } + } + if (headerBuf.size() < need) { + return; // still need more + } + headerDone = true; + } + + /* ----------- body ------------------------------------------ */ + final byte[] in = new byte[src.remaining()]; + src.get(in); + inflater.setInput(in); + + try { + int n; + while ((n = inflater.inflate(out)) > 0) { + crc.update(out, 0, n); + downstream.consume(ByteBuffer.wrap(out, 0, n)); + } + } catch (final DataFormatException ex) { + throw new IOException("Corrupt GZIP stream", ex); + } + } + + @Override + public void streamEnd(final List trailers) + throws HttpException, IOException { + if (closed.compareAndSet(false, true)) { + inflater.end(); + downstream.streamEnd(trailers); + } + } + + @Override + public void releaseResources() { + inflater.end(); + downstream.releaseResources(); + } +} 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 d5a105c7cb..7f1f2153e2 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 @@ -27,7 +27,7 @@ package org.apache.hc.client5.http.impl.async; import java.io.IOException; -import java.util.Collections; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Set; @@ -37,6 +37,7 @@ import org.apache.hc.client5.http.async.AsyncExecChain; import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.methods.InflatingAsyncDataConsumer; +import org.apache.hc.client5.http.async.methods.InflatingGzipDataConsumer; import org.apache.hc.client5.http.entity.compress.ContentCoding; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; @@ -75,13 +76,17 @@ public ContentCompressionAsyncExec( } /** - * default = DEFLATE only + * Default: DEFLATE + GZIP (plus x-gzip alias). */ public ContentCompressionAsyncExec() { final LinkedHashMap> map = new LinkedHashMap<>(); map.put(ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null)); + map.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new); + map.put(ContentCoding.X_GZIP.token(), InflatingGzipDataConsumer::new); this.decoders = RegistryBuilder.>create() + .register(ContentCoding.GZIP.token(), map.get(ContentCoding.GZIP.token())) + .register(ContentCoding.X_GZIP.token(), map.get(ContentCoding.X_GZIP.token())) .register(ContentCoding.DEFLATE.token(), map.get(ContentCoding.DEFLATE.token())) .build(); } @@ -100,7 +105,7 @@ public void execute( if (enabled && !request.containsHeader(HttpHeaders.ACCEPT_ENCODING)) { request.addHeader(MessageSupport.headerOfTokens( - HttpHeaders.ACCEPT_ENCODING, Collections.singletonList("deflate"))); + HttpHeaders.ACCEPT_ENCODING, Arrays.asList("gzip", "x-gzip", "deflate"))); } chain.proceed(request, producer, scope, new AsyncExecCallback() { diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/GzipRoundTripTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/GzipRoundTripTest.java new file mode 100644 index 0000000000..82ba4ec38b --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/async/methods/GzipRoundTripTest.java @@ -0,0 +1,172 @@ +/* + * ==================================================================== + * 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.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.nio.AsyncDataConsumer; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.CapacityChannel; +import org.apache.hc.core5.http.nio.DataStreamChannel; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer; +import org.junit.jupiter.api.Test; + +/** + * Round-trip tests for GZIP inflate / deflate helpers + * that compile and run on plain Java 8. + */ +public class GzipRoundTripTest { + + private static final String TEXT = + "Hello GZIP 🚀 – こんにちは世界 – Bonjour le monde!"; + + /* ---------------- collector that just stores all bytes ------------- */ + + private static final class Collector implements AsyncDataConsumer { + + private final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + @Override + public void updateCapacity(final CapacityChannel c) throws IOException { + } + + @Override + public void consume(final ByteBuffer src) throws IOException { + final byte[] tmp = new byte[src.remaining()]; + src.get(tmp); + buf.write(tmp); + } + + @Override + public void streamEnd(final List t) throws IOException { + } + + + @Override + public void releaseResources() { + } + + byte[] toByteArray() { + return buf.toByteArray(); + } + } + + /* ------------------------------------------------------------------ */ + + @Test + void gzipDecompress() throws Exception { + + /* compressed reference data */ + final ByteArrayOutputStream gz = new ByteArrayOutputStream(); + try (final GZIPOutputStream gos = new GZIPOutputStream(gz)) { + gos.write(TEXT.getBytes(StandardCharsets.UTF_8)); + } + + final Collector inner = new Collector(); + final InflatingGzipDataConsumer gunzip = new InflatingGzipDataConsumer(inner); + + /* feed entire stream in one go */ + gunzip.consume(ByteBuffer.wrap(gz.toByteArray())); + gunzip.streamEnd(Collections.
emptyList()); // notify EOF + + final String out = new String(inner.toByteArray(), StandardCharsets.UTF_8); + assertEquals(TEXT, out); + } + + /* ------------------------------------------------------------------ */ + + @Test + void gzipCompress() throws Exception { + + final AsyncEntityProducer json = + new StringAsyncEntityProducer( + "\"" + TEXT + "\"", + ContentType.APPLICATION_JSON); + + final AsyncEntityProducer gzipProd = new DeflatingGzipEntityProducer(json); + + /* collect bytes “on the wire” */ + final ByteArrayOutputStream wire = new ByteArrayOutputStream(); + final CountDownLatch done = new CountDownLatch(1); + + gzipProd.produce(new DataStreamChannel() { + @Override + public void requestOutput() { + } + + @Override + public int write(final ByteBuffer src) { + final byte[] tmp = new byte[src.remaining()]; + src.get(tmp); + wire.write(tmp, 0, tmp.length); + return tmp.length; + } + + @Override + public void endStream() { + done.countDown(); + } + + @Override + public void endStream(final List t) { + endStream(); + } + }); + + if (!done.await(2, TimeUnit.SECONDS)) { + fail("producer timed-out"); + } + + /* verify round-trip */ + final ByteArrayInputStream bin = new ByteArrayInputStream(wire.toByteArray()); + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + final byte[] buf = new byte[4096]; + try (final GZIPInputStream gin = new GZIPInputStream(bin)) { + int n; + while ((n = gin.read(buf)) != -1) { + bout.write(buf, 0, n); + } + } + final String roundTrip = bout.toString(StandardCharsets.UTF_8.name()); + assertEquals("\"" + TEXT + "\"", roundTrip); + } +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipCompressionExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipCompressionExample.java new file mode 100644 index 0000000000..b4f4d8c394 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipCompressionExample.java @@ -0,0 +1,77 @@ +/* + * ==================================================================== + * 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.examples; + +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.async.methods.DeflatingGzipEntityProducer; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; + +/** + *

Example – streaming GZIP compression with the async API

+ * + *

{@link DeflatingGzipEntityProducer} wraps any existing + * {@link org.apache.hc.core5.http.nio.AsyncEntityProducer} and emits a + * fully-valid GZIP stream on the wire while honouring back-pressure.

+ * + *

This example sends a small JSON document compressed with GZIP to + * httpbin / post and prints the + * server’s echo response. + * The {@code Content-Encoding: gzip} header is added automatically by + * {@code RequestContent} interceptor — no manual header work required.

+ * + * @since 5.6 + */ +public final class AsyncClientGzipCompressionExample { + + public static void main(final String[] args) throws Exception { + try (final CloseableHttpAsyncClient client = HttpAsyncClients.createDefault()) { + client.start(); + + final SimpleHttpRequest req = SimpleRequestBuilder + .get("https://httpbin.org/gzip") + .build(); + + final Future> f = client.execute( + SimpleRequestProducer.create(req), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + null); + + final Message msg = f.get(); + System.out.println("status=" + msg.getHead().getCode()); + System.out.println("body=\n" + msg.getBody()); + } + } +} \ No newline at end of file diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipDecompressionExample.java b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipDecompressionExample.java new file mode 100644 index 0000000000..b68975439d --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientGzipDecompressionExample.java @@ -0,0 +1,99 @@ +/* + * ==================================================================== + * 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.examples; + +import java.util.concurrent.Future; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.async.methods.SimpleRequestProducer; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.BasicResponseConsumer; + +/** + *

Example – transparent GZIP de-compression with the async API

+ * + *

{@code ContentCompressionAsyncExec} is included by default in + * {@link HttpAsyncClients#createDefault()}. + * The interceptor adds the header + * {@code Accept-Encoding: gzip, deflate} and automatically wraps the + * downstream consumer in a {@code InflatingGzipDataConsumer} when the + * server replies with {@code Content-Encoding: gzip}.

+ * + *

The example performs a single {@code GET https://httpbin.org/gzip} + * request — httpbin’s + * endpoint always returns a small GZIP-compressed JSON document. + * The body is delivered to the caller as a plain UTF-8 string without + * any additional code.

+ * + *

Run it from a {@code main(...)} method; output is written to + * {@code stdout}.

+ * + * @since 5.6 + */ +public final class AsyncClientGzipDecompressionExample { + + public static void main(final String[] args) throws Exception { + + try (final CloseableHttpAsyncClient client = HttpAsyncClients.createDefault()) { + client.start(); + + final SimpleHttpRequest request = + SimpleRequestBuilder.get("https://httpbin.org/gzip").build(); + + final Future> future = client.execute( + SimpleRequestProducer.create(request), + new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), + new FutureCallback>() { + + @Override + public void completed(final Message result) { + System.out.println(request.getRequestUri() + + " -> " + result.getHead().getCode()); + System.out.println("Decompressed body:\n" + result.getBody()); + } + + @Override + public void failed(final Exception ex) { + System.err.println(request + "->" + ex); + } + + @Override + public void cancelled() { + System.out.println(request + " cancelled"); + } + }); + + future.get(); // wait for completion + } + } +} 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 88a01f0b70..730d70402f 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 @@ -116,7 +116,7 @@ void testAcceptEncodingAdded() throws Exception { final HttpRequest request = new BasicHttpRequest(Method.GET, "/path"); executeAndCapture(request); assertTrue(request.containsHeader(HttpHeaders.ACCEPT_ENCODING)); - assertEquals("deflate", request.getFirstHeader(HttpHeaders.ACCEPT_ENCODING).getValue()); + assertEquals("gzip, x-gzip, deflate", request.getFirstHeader(HttpHeaders.ACCEPT_ENCODING).getValue()); } @Test