Guide to Calling HTTP APIs with HttpClientProxy

Updated at 1780841105000
HttpClientProxy is a wrapper around HttpClient that supports two ways of calling HTTP APIs: synchronous calls with timeout and asynchronous calls with callbacks. For normal HTTP requests, the proxy does not execute the request immediately. Instead, it pushes the request into a queue. Worker threads then take requests from the queue, execute them through HttpClient, and complete the corresponding future or callback.
sequenceDiagram
    participant App as Application
    participant Proxy as HttpClientProxy
    participant Queue as RequestQueue
    participant Worker as Worker thread
    participant Client as HttpClient
    participant API as HTTP API

    App->>Proxy: call/request/fire/execute(request)
    Proxy->>Queue: add request
    Worker->>Queue: take request
    Worker->>Client: client.request(request)
    Client->>API: HTTP request
    API-->>Client: HTTP response
    Client-->>Worker: ResponseEntity
    Worker-->>Proxy: complete future/callback
    Proxy-->>App: body / ResponseEntity / callback

Creating HttpClientProxy

You can create a proxy using the builder:
HttpClientProxy client = HttpClientProxy.builder()
    .threadPoolSize(8)
    .requestQueueCapacity(10_000)
    .connectTimeout(15_000)
    .readTimeout(15_000)
    .autoStart(true)
    .build();
If autoStart(true) is not enabled, call start() before sending requests:
HttpClientProxy client = HttpClientProxy.builder()
    .threadPoolSize(8)
    .requestQueueCapacity(10_000)
    .build();

client.start();
When the proxy is no longer needed, close it to stop worker threads, clear the queue, and cancel unfinished requests:
client.close();
// or
client.stop();
Note: call, request, fire, and execute go through the HttpClientProxy queue. The download and upload methods currently delegate directly to HttpClient, so they run synchronously on the calling thread.

Creating A Request

Basic request types include GetRequest, PostRequest, PutRequest, and DeleteRequest. A request can define its URL, body, headers, timeout, and response type by status code.
GET example:
GetRequest request = new GetRequest()
    .setURL("https://api.example.com/users/1")
    .setResponseType(UserResponse.class);
POST JSON example:
CreateUserRequest body = new CreateUserRequest("Alice");

PostRequest request = new PostRequest()
    .setURL("https://api.example.com/users")
    .setEntity(
        RequestEntity.builder()
            .contentType("application/json")
            .header("Authorization", "Bearer " + accessToken)
            .body(body)
            .build()
    )
    .setResponseType(201, UserResponse.class);
If Content-Type is not specified, the request body is serialized as JSON by default. The response body is deserialized based on the response Content-Type. If setResponseType(statusCode, SomeClass.class) is configured, the body for that status code is parsed into the given class.

call: Call An API And Return The Body

call(Request request, int timeout) sends the request through the queue, waits up to timeout, and returns the response body.
UserResponse user = client.call(
    new GetRequest()
        .setURL("https://api.example.com/users/1")
        .setResponseType(UserResponse.class),
    5_000
);
Use call when you only need the response body and want HTTP error statuses to be converted into exceptions. For status codes below 400, the method returns the body. For status codes from 400, it throws the corresponding exception, such as bad request, unauthorized, not found, conflict, too many requests, or internal server error.

request: Call An API And Return ResponseEntity

request(Request request, int timeout) returns the full ResponseEntity, including status, headers, and body.
ResponseEntity response = client.request(
    new GetRequest()
        .setURL("https://api.example.com/users/1")
        .setResponseType(UserResponse.class),
    5_000
);

int status = response.getStatus();
Object body = response.getBody();
Use request when you need to inspect the status code or headers, such as pagination, rate limit, Location, or ETag headers.

fire: Asynchronous Call Returning Body

fire(Request request, RequestCallback callback) sends the request through the queue and returns the result through a callback. The callback receives the response body, not the full ResponseEntity.
client.fire(
    new GetRequest()
        .setURL("https://api.example.com/users/1")
        .setResponseType(UserResponse.class),
    new RequestCallback<UserResponse>() {
        @Override
        public void onResponse(UserResponse response) {
            System.out.println("User: " + response.getName());
        }

        @Override
        public void onException(Exception e) {
            e.printStackTrace();
        }
    }
);
Use fire when you want a non-blocking API call and only care about the response body.

execute: Asynchronous Call Returning ResponseEntity

execute(Request request, RequestCallback<ResponseEntity> callback) is similar to fire, but the callback receives the full ResponseEntity.
client.execute(
    new GetRequest()
        .setURL("https://api.example.com/users/1")
        .setResponseType(UserResponse.class),
    new RequestCallback<ResponseEntity>() {
        @Override
        public void onResponse(ResponseEntity response) {
            System.out.println("Status: " + response.getStatus());
            System.out.println("Body: " + response.getBody());
        }

        @Override
        public void onException(Exception e) {
            e.printStackTrace();
        }
    }
);
Use execute when you need to process the status code, headers, and body in the callback.

Download A File By URL

The simplest download method downloads a file from a URL into a directory. The file name is taken from the Content-Disposition header. If that header is not available, the name is derived from the URL.
String fileName = client.download(
    "https://example.com/files/report.pdf",
    new File("/tmp/downloads")
);

System.out.println("Downloaded: " + fileName);
You can pass a DownloadCancellationToken to cancel the download:
DownloadCancellationToken token = new DownloadCancellationToken();

String fileName = client.download(
    "https://example.com/files/report.pdf",
    new File("/tmp/downloads"),
    token
);

// from another thread
token.cancel();

Download With DownloadRequest

Use DownloadRequest when you need custom timeout or headers.
DownloadRequest request = new DownloadRequest()
    .setFileURL("https://example.com/files/report.pdf")
    .setConnectTimeout(5_000)
    .setReadTimeout(30_000);

String fileName = client.download(
    request,
    new File("/tmp/downloads")
);

Download Into OutputStream

If you do not want to save directly to a file, you can write the response into an OutputStream.
try (OutputStream outputStream = Files.newOutputStream(Paths.get("/tmp/report.pdf"))) {
    client.download(
        "https://example.com/files/report.pdf",
        outputStream
    );
}
This variant also supports DownloadRequest and DownloadCancellationToken.

Download With A Custom File Name

When a custom fileName is provided, the client saves the file using the new name while keeping the original extension if it can be detected.
DownloadFileResult result = client.download(
    "https://example.com/files/monthly-report.pdf",
    new File("/tmp/downloads"),
    "report-2026-06"
);

System.out.println(result.getOriginalFileName()); // monthly-report.pdf
System.out.println(result.getNewFileName());      // report-2026-06.pdf
This variant returns DownloadFileResult, which contains both the original file name and the new file name.

callUpload: Upload A File And Return The Body

callUpload(UploadRequest request) uploads a file using multipart form-data and returns the response body.
UploadRequest request = new UploadRequest()
    .setURL("https://api.example.com/files")
    .setFilePath("/tmp/report.pdf")
    .setFileName("report.pdf")
    .setResponseType(UploadResponse.class);

UploadResponse response = client.callUpload(request);
You can also upload from an InputStream:
try (InputStream inputStream = Files.newInputStream(Paths.get("/tmp/report.pdf"))) {
    UploadRequest request = new UploadRequest()
        .setURL("https://api.example.com/files")
        .setInputStream(inputStream)
        .setFileName("report.pdf")
        .setResponseType(UploadResponse.class);

    UploadResponse response = client.callUpload(request);
}
The file is sent as a multipart field named file.

upload: Upload A File And Return ResponseEntity

upload(UploadRequest request) returns the full response.
ResponseEntity response = client.upload(
    new UploadRequest()
        .setURL("https://api.example.com/files")
        .setFilePath("/tmp/report.pdf")
        .setFileName("report.pdf")
        .setResponseType(UploadResponse.class)
);

System.out.println(response.getStatus());
System.out.println(response.getBody());
You can pass an UploadCancellationToken to cancel the upload:
UploadCancellationToken token = new UploadCancellationToken();

ResponseEntity response = client.upload(
    new UploadRequest()
        .setURL("https://api.example.com/files")
        .setFilePath("/tmp/report.pdf")
        .setFileName("report.pdf")
        .setResponseType(UploadResponse.class),
    token
);

// from another thread
token.cancel();

Usage Notes

HttpClientProxy must be active before calling call, request, fire, or execute. If it has not been started or has already been closed, the proxy throws a client-not-active error.
The request queue has a fixed capacity. When the number of waiting requests exceeds requestQueueCapacity, new requests are rejected with a queue-full error.
For call and request, the timeout parameter is the maximum time to wait for the future result. HTTP connect timeout and read timeout are configured separately through the builder or on each request.
A typical lifecycle looks like this:
try {
    // call APIs
} finally {
    client.close();
}

Table Of Contents