class HttpClientReadableByteChannel implements ReadableByteChannel, AsyncBody.Consumer<List<ByteBuffer>> {

  private final LinkedList<ByteBuffer> buffers = new LinkedList<>();
  private Throwable failed;
  private boolean closed;
  private boolean done;
  private CompletableFuture<AsyncBody> asyncBodyFuture = new CompletableFuture<>();
  private ByteBuffer currentBuffer;
  private ReentrantLock lock = new ReentrantLock();
  private Condition condition = lock.newCondition();

  @Override
  public void consume(List<ByteBuffer> value, AsyncBody asyncBody) throws Exception {
    doLockedAndSignal(() -> this.buffers.addAll(value));
  }

  protected void onResponse(HttpResponse<AsyncBody> response) {<FILL_FUNCTION_BODY>}

  private void onBodyDone(Void v, Throwable t) {
    doLockedAndSignal(() -> {
      if (t != null) {
        failed = t;
      }
      done = true;
      return null;
    });
  }

  <T> T doLockedAndSignal(Supplier<T> run) {
    lock.lock();
    try {
      condition.signalAll();
      return run.get();
    } finally {
      lock.unlock();
    }
  }

  @Override
  public void close() {
    if (doLockedAndSignal(() -> {
      if (this.closed) {
        return false;
      }
      this.closed = true;
      return true;
    })) {
      asyncBodyFuture.thenAccept(AsyncBody::cancel);
    }
  }

  @Override
  public synchronized boolean isOpen() {
    return !closed;
  }

  @Override
  public int read(ByteBuffer arg0) throws IOException {
    lock.lock();
    try {
      if (closed) {
        throw new ClosedChannelException();
      }

      int read = 0;

      while (arg0.hasRemaining()) {
        while (currentBuffer == null || !currentBuffer.hasRemaining()) {
          if (buffers.isEmpty()) {
            if (failed != null) {
              throw new IOException("channel already closed with exception", failed);
            }
            if (read > 0) {
              return read;
            }
            if (done) {
              return -1;
            }
            lock.unlock();
            try {
              // relinquish the lock to consume more
              this.asyncBodyFuture.thenAccept(AsyncBody::consume);
            } finally {
              lock.lock();
            }
            try {
              while (!done && buffers.isEmpty()) {
                condition.await(); // block until more buffers are delivered
              }
            } catch (InterruptedException e) {
              close();
              Thread.currentThread().interrupt();
              throw new ClosedByInterruptException();
            }
          }

          currentBuffer = buffers.poll();
        }

        int remaining = Math.min(arg0.remaining(), currentBuffer.remaining());
        for (int i = 0; i < remaining; i++) {
          arg0.put(currentBuffer.get());
        }
        read += remaining;
      }

      return read;
    } finally {
      if (lock.isHeldByCurrentThread()) {
        lock.unlock();
      }
    }
  }

}

    AsyncBody asyncBody = response.body();
    asyncBodyFuture.complete(asyncBody);
    asyncBody.done().whenComplete(this::onBodyDone);
    asyncBody.consume(); // pre-fetch the first results
    doLockedAndSignal(() -> null);
  
<no_super_class>