GitHub - AsyncHttpClient/async-http-client: Asynchronous Http and WebSocket Client library for Java
AsyncHttpClient (AHC) is a high-performance, asynchronous HTTP client for Java built on top of Netty. It supports HTTP/1.1, HTTP/2, and WebSocket protocols.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Configuration
- HTTP Requests
- Handling Responses
- HTTP/2
- WebSocket
- Authentication
- Proxy Support
- Community
- License
Features
- HTTP/2 with multiplexing — enabled by default over TLS via ALPN, with connection multiplexing and GOAWAY handling
- HTTP/1.1 and HTTP/1.0 — connection pooling and keep-alive
- WebSocket — text, binary, and ping/pong frame support
- Asynchronous API — non-blocking I/O with
ListenableFutureandCompletableFuture - Compression — automatic gzip, deflate, Brotli, and Zstd decompression
- Authentication — Basic, Digest, NTLM, SPNEGO/Kerberos, and SCRAM-SHA-256
- Proxy — HTTP, SOCKS4, and SOCKS5 with CONNECT tunneling
- Native transports — optional Epoll, KQueue, and io_uring
- Request/response filters — intercept and transform at each stage
- Cookie management — RFC 6265-compliant cookie store
- Multipart uploads — file, byte array, input stream, and string parts
- Resumable downloads — built-in
ResumableIOExceptionFilter
Requirements
Java 11+
Installation
Maven:
<dependency> <groupId>org.asynchttpclient</groupId> <artifactId>async-http-client</artifactId> <version>3.0.11</version> </dependency>
Gradle:
implementation 'org.asynchttpclient:async-http-client:3.0.11'Optional: Native Transport
For lower-latency I/O on Linux, add a native transport dependency:
<!-- Epoll (Linux) --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-epoll</artifactId> <classifier>linux-x86_64</classifier> </dependency> <!-- io_uring (Linux) --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-io_uring</artifactId> <classifier>linux-x86_64</classifier> </dependency>
Then enable in config:
AsyncHttpClient client = asyncHttpClient(config().setUseNativeTransport(true));
Optional: Brotli / Zstd Compression
<dependency> <groupId>com.aayushatharva.brotli4j</groupId> <artifactId>brotli4j</artifactId> <version>1.20.0</version> </dependency> <dependency> <groupId>com.github.luben</groupId> <artifactId>zstd-jni</artifactId> <version>1.5.7-7</version> </dependency>
Quick Start
Import the DSL helpers:
import static org.asynchttpclient.Dsl.*;
Create a client, execute a request, and read the response:
try (AsyncHttpClient client = asyncHttpClient()) { // Asynchronous client.prepareGet("https://www.example.com/") .execute() .toCompletableFuture() .thenApply(Response::getResponseBody) .thenAccept(System.out::println) .join(); // Synchronous (blocking) Response response = client.prepareGet("https://www.example.com/") .execute() .get(); }
Note:
AsyncHttpClientinstances are long-lived, shared resources. Always close them when done. Creating a new client per request will degrade performance due to repeated thread pool and connection pool creation.
Configuration
Use config() to build an AsyncHttpClientConfig:
AsyncHttpClient client = asyncHttpClient(config() .setConnectTimeout(Duration.ofSeconds(5)) .setRequestTimeout(Duration.ofSeconds(30)) .setMaxConnections(500) .setMaxConnectionsPerHost(100) .setFollowRedirect(true) .setMaxRedirects(5) .setCompressionEnforced(true));
HTTP Requests
Sending Requests
Bound — build directly from the client:
Response response = client .prepareGet("https://api.example.com/users") .addHeader("Accept", "application/json") .addQueryParam("page", "1") .execute() .get();
Unbound — build standalone via DSL, then execute:
Request request = get("https://api.example.com/users") .addHeader("Accept", "application/json") .addQueryParam("page", "1") .build(); Response response = client.executeRequest(request).get();
Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE.
Request Bodies
Use setBody to attach a body. Supported types:
| Type | Description |
|---|---|
String |
Text content |
byte[] |
Raw bytes |
ByteBuffer |
NIO buffer |
InputStream |
Streaming input |
File |
File content |
Publisher<ByteBuf> |
Reactive stream |
BodyGenerator |
Custom body generation |
Response response = client .preparePost("https://api.example.com/data") .setHeader("Content-Type", "application/json") .setBody("{\"name\": \"value\"}") .execute() .get();
For streaming bodies, see FeedableBodyGenerator which lets you push chunks
asynchronously.
Multipart Uploads
Response response = client .preparePost("https://api.example.com/upload") .addBodyPart(new FilePart("file", new File("report.pdf"), "application/pdf")) .addBodyPart(new StringPart("description", "Monthly report")) .execute() .get();
Part types: FilePart, ByteArrayPart, InputStreamPart, StringPart.
Handling Responses
Blocking
Response response = client.prepareGet("https://www.example.com/").execute().get();
Useful for debugging, but defeats the purpose of an async client in production.
ListenableFuture
execute() returns a ListenableFuture that supports completion listeners:
ListenableFuture<Response> future = client .prepareGet("https://www.example.com/") .execute(); future.addListener(() -> { Response response = future.get(); System.out.println(response.getStatusCode()); }, executor);
If
executorisnull, the callback runs on the Netty I/O thread. Never block inside I/O thread callbacks.
CompletableFuture
client.prepareGet("https://www.example.com/") .execute() .toCompletableFuture() .thenApply(Response::getResponseBody) .thenAccept(System.out::println) .join();
AsyncCompletionHandler
For most async use cases, extend AsyncCompletionHandler — it buffers the
full response and gives you a single onCompleted(Response) callback:
client.prepareGet("https://www.example.com/") .execute(new AsyncCompletionHandler<String>() { @Override public String onCompleted(Response response) { return response.getResponseBody(); } });
AsyncHandler
For fine-grained control, implement AsyncHandler directly. This lets you
inspect status, headers, and body chunks as they arrive and abort early:
Future<Integer> future = client .prepareGet("https://www.example.com/") .execute(new AsyncHandler<>() { private int status; @Override public State onStatusReceived(HttpResponseStatus s) { status = s.getStatusCode(); return State.CONTINUE; } @Override public State onHeadersReceived(HttpHeaders headers) { return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart part) { return State.ABORT; // stop early — we only needed the status } @Override public Integer onCompleted() { return status; } @Override public void onThrowable(Throwable t) { t.printStackTrace(); } });
HTTP/2
HTTP/2 is enabled by default for HTTPS connections via ALPN negotiation. The client uses HTTP/2 when the server supports it and falls back to HTTP/1.1 otherwise. No additional configuration is required.
- Connection multiplexing — concurrent streams over a single TCP connection
- GOAWAY handling — graceful connection draining on server shutdown
- PING keepalive — configurable ping frames to keep connections alive
HTTP/2 Configuration
AsyncHttpClient client = asyncHttpClient(config() .setHttp2MaxConcurrentStreams(100) .setHttp2InitialWindowSize(65_535) .setHttp2MaxFrameSize(16_384) .setHttp2MaxHeaderListSize(8_192) .setHttp2PingInterval(Duration.ofSeconds(30)) // keepalive pings .setHttp2CleartextEnabled(true)); // h2c prior knowledge
To force HTTP/1.1, disable HTTP/2:
AsyncHttpClient client = asyncHttpClient(config().setHttp2Enabled(false));
WebSocket
WebSocket ws = client .prepareGet("wss://echo.example.com/") .execute(new WebSocketUpgradeHandler.Builder() .addWebSocketListener(new WebSocketListener() { @Override public void onOpen(WebSocket ws) { ws.sendTextFrame("Hello!"); } @Override public void onTextFrame(String payload, boolean finalFragment, int rsv) { System.out.println(payload); } @Override public void onClose(WebSocket ws, int code, String reason) {} @Override public void onError(Throwable t) { t.printStackTrace(); } }) .build()) .get();
Authentication
// Client-wide Basic auth AsyncHttpClient client = asyncHttpClient(config() .setRealm(basicAuthRealm("user", "password"))); // Per-request Digest auth Response response = client .prepareGet("https://api.example.com/protected") .setRealm(digestAuthRealm("user", "password").build()) .execute() .get(); // SCRAM-SHA-256 (RFC 7804) Response response = client .prepareGet("https://api.example.com/protected") .setRealm(scramSha256AuthRealm("user", "password").build()) .execute() .get();
Supported schemes: Basic, Digest, NTLM, SPNEGO/Kerberos, SCRAM-SHA-256.
Proxy Support
// HTTP proxy AsyncHttpClient client = asyncHttpClient(config() .setProxyServer(proxyServer("proxy.example.com", 8080))); // Authenticated proxy AsyncHttpClient client = asyncHttpClient(config() .setProxyServer(proxyServer("proxy.example.com", 8080) .setRealm(basicAuthRealm("proxyUser", "proxyPassword"))));
SOCKS4 and SOCKS5 proxies are also supported.
Community
- GitHub Discussions — questions, ideas, and general discussion
- Issue Tracker — bug reports and feature requests