Skip to content

Response Decompression

One caveat concerning Java's HTTP client is the lack of support for automatic response decompression. A workaround is to use an available InputStream decompressor (e.g. GZIPInputStream) that matches response's Content-Encoding. However, such approach is invasive as it forces you to deal with InputStreams.

The straightforward and recommended solution is to use Methanol's HTTP client, which gives you transparent response decompression for gzip & deflate out of the box.

final Methanol client = Methanol.create();

<T> HttpResponse<T> get(String url, BodyHandler<T> bodyHandler) throws IOException, InterruptedException {
  // No need to worry about adding Accept-Encoding and decompressing the response, the client does that for you!
  return client.send(MutableRequest.GET(url), bodyHandler);
}

Read on if you're interested in knowing how that's accomplished, or you want to extend decompression support.

Decoding BodyHandler

The entry point to response body decompression is MoreBodyHandlers::decoding. This method takes your desired BodyHandler and gives you one that decompresses the response body as your handler's BodySubscriber receives it.

var response = client.send(request, MoreBodyHandlers.decoding(BodyHandlers.ofString()));

It doesn't matter which BodyHandler you're using; you can have whatever response body type you want.

BodyDecoder

A BodyDecoder is a BodySubscriber with the added semantics of a Flow.Processor. It intercepts the flow of bytes on its way down from the HTTP client, decoding each List<ByteBuffer> individually. The decoded bytes are forwarded to a downstream BodySubscriber, which converts them into the desired response body.

BodyDecoder.Factory

A BodyDecoder.Factory associates itself with a defined encoding that's suitable as a Content-Encoding directive. It creates BodyDecoder instances that forward the decompressed response body to a downstream BodySubscriber.

Factories are installed as service-providers in the manner specified by Java's ServiceLoader. The handler returned by MoreBodyHandlers::decoding looks up a factory matching the response's Content-Encoding to wrap user's BodySubscriber. If no such factory is found, an UnsupportedOperationException is thrown.

Supported Encodings

The core module has support for gzip & deflate out of the box. There's also a separate module for brotli.

Extending decompression support

Adding support for more encodings or overriding supported ones is a matter of writing a BodyDecoder implementation and providing a corresponding factory. However, implementing the decoder's Flow.Publisher semantics can be tricky. Instead, implement an AsyncDecoder and wrap it in an AsyncBodyDecoder, so you're only concerned with the decompression logic.

Writing an AsyncDecoder

Decoding is done as a number of decode(source, sink) rounds finalized by one final round, each with the currently available input. After the final round, your AsyncDecoder must've completely exhausted its source. Here's a decoder implementation that uses jzlib for gzip & deflate decompression.

class JZlibDecoder implements AsyncDecoder {
  private static final int BUFFER_SIZE = 8096;

  private final String encoding;
  private final com.jcraft.jzlib.Inflater inflater;
  private final ByteBuffer input = ByteBuffer.allocate(BUFFER_SIZE);
  private final ByteBuffer output = ByteBuffer.allocate(BUFFER_SIZE);

  JZlibDecoder(String encoding, com.jcraft.jzlib.JZlib.WrapperType wrapperType) {
    try {
      this.encoding = encoding;
      inflater = new com.jcraft.jzlib.Inflater(wrapperType);
      inflater.setInput(input.array());
      inflater.setOutput(output.array());
    } catch (com.jcraft.jzlib.GZIPException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public String encoding() {
    return encoding;
  }

  @Override
  public void decode(ByteSource source, ByteSink sink) throws IOException {
    synchronized (inflater) {
      while (source.hasRemaining()) {
        // Prepare input for this iteration
        refillInput(source);
        inflater.setNextInIndex(0);
        inflater.setAvailIn(input.limit());

        // Continue inflating as long as there's more input or there's pending output

        boolean mightHavePendingOutput = false;

        inflateLoop:
        while (inflater.getAvailIn() > 0 || mightHavePendingOutput) {
          // Prepare for new output
          inflater.setNextOutIndex(0);
          inflater.setAvailOut(output.capacity());

          int status = inflater.inflate(com.jcraft.jzlib.JZlib.Z_SYNC_FLUSH);
          int outputCount = inflater.getNextOutIndex();
          sink.pushBytes(output.position(0).limit(outputCount));

          switch (status) {
            case com.jcraft.jzlib.JZlib.Z_OK:
              mightHavePendingOutput = inflater.getAvailOut() == 0;
              break;

            case com.jcraft.jzlib.JZlib.Z_STREAM_END:
              // The compressed stream has ended
              break inflateLoop;

            default:
              throw new IOException("problem with zlib: " + Integer.toHexString(status));
          }
        }
      }
    }
  }

  private void refillInput(ByteSource source) {
    input.clear();
    source.pullBytes(input);
    input.flip();
  }

  @Override
  public synchronized void close() {
    synchronized (inflater) {
      inflater.end();
    }
  }
}

Registering a Factory

Here's a BodyDecoder.Factory for gzip using our jzlib decoder.

public static final class MyDecoderFactory implements BodyDecoder.Factory {
  @Override
  public String encoding() {
    return "gzip";
  }

  @Override
  public <T> BodyDecoder<T> create(BodySubscriber<T> downstream) {
    return new AsyncBodyDecoder<>(
        new JZlibDecoder("gzip", com.jcraft.jzlib.JZlib.WrapperType.GZIP), downstream);
  }

  @Override
  public <T> BodyDecoder<T> create(BodySubscriber<T> downstream, Executor executor) {
    return new AsyncBodyDecoder<>(
        new JZlibDecoder("gzip", com.jcraft.jzlib.JZlib.WrapperType.GZIP), downstream);
  }
}

The next step is to declare your factory as a service-provider. If your application uses Java modules, you'd have a declaration like the following in your module-info.java.

module my.module {
  ...

  provides BodyDecoder.Factory with MyDecoderFactory;
}