A lightweight wrapper to the JDK 11+ Java Http Client
- Requires Java 11+
- Adds a fluid API for building URLs and payloads
- Adds JSON marshalling/unmarshalling of request/response using avaje-jsonb, Jackson, Moshi, or Gson
- Gzip encoding/decoding
- Logging of request/response logging
- Interception of request/response
- Built in support for authorization via Basic Auth and Bearer Tokens
- Provides async and sync API
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-client</artifactId>
<version>${avaje.client.version}</version>
</dependency>
Create a HttpClient with a baseUrl, Jackson or Gson based JSON body adapter, logger.
HttpClient client = HttpClient.builder()
.baseUrl(baseUrl)
.bodyAdapter(new JsonbBodyAdapter())
//.bodyAdapter(new JacksonBodyAdapter(new ObjectMapper()))
//.bodyAdapter(new GsonBodyAdapter(new Gson()))
.build();
HttpResponse<String> hres = client.request()
.path("hello")
.GET()
.asString();
From HttpClient:
-
Create a request
-
Build the url via path(), matrixParam(), queryParam()
-
Optionally set headers(), cookies() etc
-
Optionally specify a request body (JSON, form, or any JDK BodyPublisher)
-
Http verbs - GET(), POST(), PUT(), PATCH(), DELETE(), HEAD(), TRACE()
-
Sync processing response body as:
- a bean, list of beans, stream of beans, String, Void or any JDK Response.BodyHandler
-
Async processing of the request using CompletableFuture
- a bean, list of beans, stream of beans, String, Void or any JDK Response.BodyHandler
-
Introduction to JDK HttpClient at JDK HttpClient Introduction
-
Javadoc for JDK HttpClient
HttpClient client = HttpClient.builder()
.baseUrl(baseUrl)
.build();
HttpResponse<String> hres = client.request()
.path("hello")
.GET()
.asString();
HttpResponse<CustomerDto> customer = client.request()
.path("customers").path(42)
.GET()
.as(CustomerDto.class);
// just get the bean without HttpResponse
CustomerDto customer = client.request()
.path("customers").path(42)
.GET()
.bean(CustomerDto.class);
// get a List
HttpResponse<List<CustomerDto>> customers = client.request()
.path("customers")
.queryParam("active", "true")
.GET()
.asList(CustomerDto.class);
// get a Stream - `application/x-json-stream`
HttpResponse<List<CustomerDto>> customers = client.request()
.path("customers/stream")
.GET()
.asStream(CustomerDto.class);
- All async requests use CompletableFuture<T>
- throwable is a CompletionException
- In the example below hres is of type HttpResponse<String>
client.request()
.path("hello")
.GET()
.async().asString() // CompletableFuture<HttpResponse<String>>
.whenComplete((hres, throwable) -> {
if (throwable != null) {
// CompletionException
...
} else {
// HttpResponse<String>
int statusCode = hres.statusCode();
String body = hres.body();
...
}
});
Overview of response types for sync calls.
sync processing | |
asVoid | HttpResponse<Void> |
asString | HttpResponse<String> |
as<E> | HttpResponse<E> |
asList<E> | HttpResponse<List<E>> |
asStream<E> | HttpResponse<Stream<E>> |
bean<E> | E |
list<E> | List<E> |
stream<E> | Stream<E> |
handler(HttpResponse.BodyHandler<E>) | HttpResponse<E> |
async processing | |
asVoid | CompletableFuture<HttpResponse<Void>> |
asString | CompletableFuture<HttpResponse<String>> |
as<E> | CompletableFuture<HttpResponse<E>> |
asList<E> | CompletableFuture<HttpResponse<List<E>>> |
asStream<E> | CompletableFuture<HttpResponse<Stream<E>>> |
bean<E> | CompletableFuture<E> |
list<E> | CompletableFuture<List<E>> |
stream<E> | CompletableFuture<Stream<E>> |
handler(HttpResponse.BodyHandler<E>) | CompletableFuture<HttpResponse<E>> |
JDK HttpClient provides a number of BodyHandlers
including reactive Flow-based subscribers. With the handler()
method we can use any of these or our own HttpResponse.BodyHandler
implementation.
Refer to HttpResponse.BodyHandlers
discarding() | Discards the response body |
ofByteArray() | byte[] |
ofString() | String, additional charset option |
ofLines() | Stream<String> |
ofInputStream() | InputStream |
ofFile(Path file) | Path with various options |
ofByteArrayConsumer(...) | |
fromSubscriber(...) | various options |
fromLineSubscriber(...) | various options |
When sending body content we can use:
- Object which is written as JSON content by default
- byte[], String, Path (file), InputStream
- formParams() for url encoded form (
application/x-www-form-urlencoded
) - Any HttpRequest.BodyPublisher
HttpResponse<String> hres = client.request()
.path("hello")
.GET()
.asString();
- All async requests use JDK httpClient.sendAsync(...) returning CompletableFuture<T>
- throwable is a CompletionException
- In the example below hres is of type HttpResponse<String>
client.request()
.path("hello")
.GET()
.async().asString()
.whenComplete((hres, throwable) -> {
if (throwable != null) {
// CompletionException
...
} else {
// HttpResponse<String>
int statusCode = hres.statusCode();
String body = hres.body();
...
}
});
HttpResponse<Customer> customer = client.request()
.path("customers").path(42)
.GET()
.as(Customer.class);
Customer customer = client.request()
.path("customers").path(42)
.GET()
.bean(Customer.class);
HttpResponse<List<Customer>> list = client.request()
.path("customers")
.GET()
.asList(Customer.class);
List<Customer> list = client.request()
.path("customers")
.GET()
.list(Customer.class);
HttpResponse<Stream<Customer>> stream = client.request()
.path("customers/all")
.GET()
.asStream(Customer.class);
Stream<Customer> stream = client.request()
.path("customers/all")
.GET()
.stream(Customer.class);
Hello bean = new Hello(42, "rob", "other");
HttpResponse<Void> res = client.request()
.path("hello")
.body(bean)
.POST()
.asDiscarding();
assertThat(res.statusCode()).isEqualTo(201);
Multiple calls to path()
append with a /
. This is make it easier to build a path
programmatically and also build paths that include matrixParam()
HttpResponse<String> res = client.request()
.path("customers")
.path("42")
.path("contacts")
.GET()
.asString();
// is the same as ...
HttpResponse<String> res = client.request()
.path("customers/42/contacts")
.GET()
.asString();
HttpResponse<String> httpRes = client.request()
.path("books")
.matrixParam("author", "rob")
.matrixParam("country", "nz")
.path("foo")
.matrixParam("extra", "banana")
.GET()
.asString();
List<Product> beans = client.request()
.path("products")
.queryParam("sortBy", "name")
.queryParam("maxCount", "100")
.GET()
.list(Product.class);
HttpResponse<Void> res = client.request()
.path("register/user")
.formParam("name", "Bazz")
.formParam("email", "[email protected]")
.formParam("url", "http://foo.com")
.formParam("startDate", "2020-12-03")
.POST()
.asDiscarding();
assertThat(res.statusCode()).isEqualTo(201);
To add Retry funtionality, use .retryHandler(yourhandler)
on the builder to provide your retry handler. The RetryHandler
interface provides two methods, one for status exceptions (e.g. you get a 4xx/5xx from the server) and another for exceptions thrown by the underlying client (e.g. server times out or client couldn't send request). Here is example implementation of RetryHandler
.
public final class ExampleRetry implements RetryHandler {
private static final int MAX_RETRIES = 2;
@Override
public boolean isRetry(int retryCount, HttpResponse<?> response) {
final var code = response.statusCode();
if (retryCount >= MAX_RETRIES || code <= 400) {
return false;
}
return true;
}
@Override
public boolean isExceptionRetry(int retryCount, HttpException response) {
//unwrap the exception
final var cause = response.getCause();
if (retryCount >= MAX_RETRIES) {
return false;
}
if (cause instanceof ConnectException) {
return true;
}
return false;
}
All async requests use JDK httpClient.sendAsync(...) returning CompletableFuture. Commonly the
whenComplete()
callback is used to process the async responses.
The bean()
, list()
and stream()
responses throw a HttpException
if the status code >= 300
(noting that by default redirects are followed apart for HTTPS to HTTP).
async processing | |
asVoid | CompletableFuture<HttpResponse<Void>> |
asString | CompletableFuture<HttpResponse<String>> |
bean<E> | CompletableFuture<E> |
list<E> | CompletableFuture<List<E>> |
stream<E> | CompletableFuture<Stream<E>> |
handler(HttpResponse.BodyHandler<E>) | CompletableFuture<HttpResponse<E>> |
client.request()
.path("hello/world")
.GET()
.async().asDiscarding()
.whenComplete((hres, throwable) -> {
if (throwable != null) {
...
} else {
int statusCode = hres.statusCode();
...
}
});
client.request()
.path("hello/world")
.GET()
.async().asString()
.whenComplete((hres, throwable) -> {
if (throwable != null) {
...
} else {
int statusCode = hres.statusCode();
String body = hres.body();
...
}
});
client.request()
...
.POST().async()
.bean(HelloDto.class)
.whenComplete((helloDto, throwable) -> {
if (throwable != null) {
HttpException httpException = (HttpException) throwable.getCause();
int statusCode = httpException.getStatusCode();
// maybe convert json error response body to a bean (using Jackson/Gson)
MyErrorBean errorResponse = httpException.bean(MyErrorBean.class);
..
} else {
// use helloDto
...
}
});
The example below is a line subscriber processing response content line by line.
CompletableFuture<HttpResponse<Void>> future = client.request()
.path("hello/lineStream")
.GET().async()
.handler(HttpResponse.BodyHandlers.fromLineSubscriber(new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(String item) {
// process the line of response content
...
}
@Override
public void onError(Throwable throwable) {
...
}
@Override
public void onComplete() {
...
}
}))
.whenComplete((hres, throwable) -> {
int statusCode = hres.statusCode();
...
});
If we are creating an API and want the client code to choose to execute
the request asynchronously or synchronously then we can use call()
.
The client can then choose to execute()
the request synchronously or
choose async()
to execute the request asynchronously.
HttpCall<List<Customer>> call =
client.request()
.path("customers")
.GET()
.call().list(Customer.class);
// Either execute synchronously
List<Customer> customers = call.execute();
// Or execute asynchronously
call.async()
.whenComplete((customers, throwable) -> {
...
});
We can use BasicAuthIntercept
to intercept all requests by adding an Authorization: Basic ...
header ("Basic Auth").
HttpClient client =
HttpClient.builder()
.baseUrl(baseUrl)
...
.requestIntercept(new BasicAuthIntercept("myUsername", "myPassword")) <!-- HERE
.build();
For authorization using Bearer
tokens that are obtained and expire, implement AuthTokenProvider
and register that when building the HttpClient.
class MyAuthTokenProvider implements AuthTokenProvider {
@Override
public AuthToken obtainToken(HttpClientRequest tokenRequest) {
AuthTokenResponse res = tokenRequest
.url("https://foo/v2/token")
.header("content-type", "application/json")
.body(authRequestAsJson())
.POST()
.bean(AuthTokenResponse.class);
Instant validUntil = Instant.now().plusSeconds(res.expires_in).minusSeconds(60);
return AuthToken.of(res.access_token, validUntil);
}
}
HttpClient client = HttpClient.builder()
.baseUrl("https://foo")
...
.authTokenProvider(new MyAuthTokenProvider()) <!-- HERE
.build();
All requests using the HttpClient will automatically get
an Authorization
header with Bearer
token added. The token will be
obtained for the initial request and then renewed when the token has expired.