public <Data, E extends Throwable>
  void executeRequest(@NotNull String url, @NotNull Class<Data> responseDataClass,
                      @NotNull ThrowableConsumer<? super Data, ? extends IOException> responseHandler,
                      @NotNull ExceptionHandler<E> exceptionHandler,
                      @Nullable DoubleConsumer progressHandler) throws IOException, E {
    AtomicReference<Throwable> exception = new AtomicReference<>();

    HttpRequests.request(url).accept("application/json").connect(request -> {
      URLConnection urlConnection = request.getConnection();
      handleRequest(request, responseDataClass, responseHandler);

      int endPos = urlConnection.getHeaderFieldInt("X-RangeLimit-EndPos", Integer.MAX_VALUE);
      int totalEntries = urlConnection.getHeaderFieldInt("X-RangeLimit-Total", -1);
      if (endPos >= totalEntries) {
        if (progressHandler != null) {
          progressHandler.accept(1d);
        }
      }
      else {
        int totalIterations = 1 + (totalEntries - 1) / endPos;
        int threadsCount = Math.max(1, Runtime.getRuntime().availableProcessors() - 1);

        boolean useThreadPool = threadsCount > 1 && totalIterations >= 10;
        CountDownLatch cdl = useThreadPool ? new CountDownLatch(threadsCount) : null;

        AtomicInteger iterationsCounter = new AtomicInteger(0);
        AtomicInteger finishedIterations = new AtomicInteger(1);

        if (progressHandler != null) {
          progressHandler.accept(1d / totalIterations);
        }
        Runnable task = () -> {
          try {
            while (true) {
              int i = iterationsCounter.incrementAndGet();
              if (i >= totalIterations) {
                break;
              }

              try {
                HttpRequests.request(url + "&start_pos=" + (i * endPos)).accept("application/json").connect(r -> {
                  handleRequest(r, responseDataClass, responseHandler);
                  if (progressHandler != null) {
                    progressHandler.accept(1d * finishedIterations.incrementAndGet() / totalIterations);
                  }
                  return null;
                });
              }
              catch (Throwable e) {
                exception.set(e);
              }
              if (exception.get() != null) {
                break;
              }
            }
          }
          finally {
            if (useThreadPool) {
              cdl.countDown();
            }
          }
        };
        if (useThreadPool) {
          ExecutorService executorService = Executors.newFixedThreadPool(threadsCount);
          for (int i = 0; i < threadsCount; i++) {
            executorService.execute(task);
          }
          try {
            cdl.await();
          }
          catch (InterruptedException ignored) {
          }
        }
        else {
          task.run();
        }
      }
      return null;
    });

    Throwable t = exception.get();
    if (t != null) {
      exceptionHandler.handle(t);
    }
  }
public <Data, E extends Throwable>
  void executeRequest(@NotNull String url, @NotNull Class<Data> responseDataClass,
                      @NotNull ThrowableConsumer<? super Data, ? extends IOException> responseHandler,
                      @NotNull ExceptionHandler<E> exceptionHandler,
                      @Nullable DoubleConsumer progressHandler)