julian-http-client (former java-restify)
Simple, annotation-based HTTP client for Java, inspired by Feign, Retrofit, and RESTEasy projects.
The main goal is to be a good option to build HTTP requests, using regular Java abstractions in order to model an API on the client-side.
- Requirements
- Usage
1.0.0 (stable)
Java >= 11
First, to use Maven Central Snapshots repository, add:
<repositories> <repository> <id>oss.sonatype.org-snapshot</id> <url>https://oss.sonatype.org/content/repositories/snapshots</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>repositories { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") mavenContent { snapshotsOnly() } } }
<dependency>
<groupId>com.github.ljtfreitas.julian-http-client</groupId>
<artifactId>julian-http-client-core</artifactId>
<version>${version}</version>
</dependency>
dependencies {
implementation("com.github.ljtfreitas.julian-http-client:julian-http-client-core:$version")
}
No additional dependencies will be added to classpath; an implementation principle from julian-http-client
is to use only classes available on Java's API.
Of course, some additional features require additional libraries (for instance, jackson
for JSON processing). In these cases, features and extensions are available through optional plugins.
julian-http-client
relies on interface proxies and a small collection of annotations.
We need a Java interface to act as some kind of abstraction over the API that we want to consume. Then, using annotations, we specify details about the API contract and expected requests/responses.
annotation | target | details |
---|---|---|
@Path |
interface/method/parameter | @Path annotation |
@GET , @POST , @PUT , @DELETE , @PATCH , @HEAD , @TRACE , @OPTIONS |
method | HTTP method annotations |
@QueryParameter |
method/parameter | @QueryParameter annotation |
@Header |
interface/method/parameter | @Header annotation |
@Cookie |
interface/method/parameter | @Cookie annotation |
@Body |
parameter | @Body annotation |
@Callback |
parameter | @Callback annotation |
shortcut annotations | interface/method/parameter | shortcut annotations |
The @Path
annotation is used to define the endpoint path. It can be used on interface level to define a common path for all methods, or optionally on the method level.
import com.github.ljtfreitas.julian.contract.Path;
import com.github.ljtfreitas.julian.contract.GET;
@Path("/base/api") // a common path, applied to all methods (optional)
public interface MyApi {
@Path("/resource")
@GET // a HTTP method annotation is required; see below
String someResource(); // path will be "/base/api/resource"
}
// no common path - it's valid as well
public interface MyApi {
@Path("/resource")
@GET
String someResource(); // path will be just "/resource"
}
Paths can have placeholders, dynamically replaced by method parameters. @Path-related arguments must be annotated with @Path
too, and the argument's names should match with the placeholder's names; in case we want to use a different name, we can use the @Path
annotation to customize it:
import com.github.ljtfreitas.julian.contract.Path;
import com.github.ljtfreitas.julian.contract.GET;
public interface MyApi {
@Path("/resource/{param}")
@GET
String someResource(@Path String param);
@Path("/resource/{param}")
@GET
String someResource(@Path(name = "param") String anotherName);
}
to use parameter names, you must compile your code with the
-parameters
flag.
A HTTP method annotation is required. Also, we can use this same annotation to define the path:
import com.github.ljtfreitas.julian.contract.GET;
import com.github.ljtfreitas.julian.contract.POST;
import com.github.ljtfreitas.julian.contract.PUT;
import com.github.ljtfreitas.julian.contract.PATCH;
import com.github.ljtfreitas.julian.contract.DELETE;
import com.github.ljtfreitas.julian.contract.OPTIONS;
import com.github.ljtfreitas.julian.contract.HEAD;
import com.github.ljtfreitas.julian.contract.TRACE;
public interface MyApi {
@GET("/resource") // an alias to "@Path("/resource) @GET". the examples below keep the same idea
String get();
@POST("/resource")
String post();
@PUT("/resource")
String put();
@PATCH("/resource")
String patch();
@DELETE("/resource")
String delete();
@OPTIONS("/resource")
String options();
@HEAD("/resource")
String head();
@TRACE("/resource")
String trace();
}
@Path("/base/api")
public interface MyApi {
@GET("/resource")
String get(); // same rules about @Path on top of interface - the final path will be "/base/api/resource"
}
public interface MyApi {
@GET("/resource/{param}") // placeholders can be used, too
String get(@Path String param); // same rules about arguments annotated with @Path
}
We can pass query parameters in several ways. The simpler one, of course, it's just put on the path:
public interface MyApi {
@GET("/resource?param=value")
String get();
}
Another option is to use @QueryParameter
annotation to define static key-value parameters:
public interface MyApi {
@GET("/resource")
@QueryParameter(name = "param", value = "value") // the final path is /resource?param=value
String someResource();
@GET("/resource")
@QueryParameter(name = "param1", value = "value1")
@QueryParameter(name = "param2", value = "value2") // @QueryParameter is repeatable; the final path is /resource?param1=value1¶m2=value2
String someResource();
}
@QueryParameter(name = "param", value = "value") // common query parameters can be defined here
public interface MyApi {
@GET("/resource") // the final path is /resource?param=value (parameters on top are inherited for all methods)
String someResource();
}
Query parameters can be dynamic too, using method arguments:
public interface MyApi {
@GET("/resource")
String someResource(@QueryParameter String name); // /resource?name={argument value}
@GET("/resource")
String someResource(@QueryParameter("parameter-name") String name); // /resource?parameter-name={argument value}
@GET("/resource")
String someResource(@QueryParameter(name = "parameter-name") String name); // /resource?parameter-name={argument value}
@GET("/resource")
String someResource(@QueryParameter("param") Collection<String> values); // /resource?param={collection-item-0}¶m={collection-item-1}...
@GET("/resource")
String someResource(@QueryParameter("param") String[] values); // /resource?param={array-item-0}¶m={array-item-1}...
@GET("/resource")
String someResource(@QueryParameter Map<String, String> values); // map keys will be used as parameter names
@GET("/resource")
String someResource(@QueryParameter com.github.ljtfreitas.julian.QueryParameters values); // QueryParameters is an immutable map-like object
}
HTTP headers can be defined using @Header
annotation:
import com.github.ljtfreitas.julian.contract.GET;
import com.github.ljtfreitas.julian.contract.Header;
public interface MyApi {
@GET("/resource")
@Header(name = "x-header", value = "header-value")
String someResource();
@GET("/resource")
@Header(name = "x-header", value = {"header-value", "another-header-value"}) // multiple values are acceptable
@Header(name = "x-other-header", value = "header-value") // @Header is repeatable
String someResource();
}
import com.github.ljtfreitas.julian.contract.GET;
import com.github.ljtfreitas.julian.contract.Header;
@Header(name = "x-header", value = "header-value") // common headers can be defined here
public interface MyApi {
@GET("/resource")
String someResource(); // x-header will be sent on HTTP request (headers on top are inherited for all methods)
}
Headers can be dynamic:
public interface MyApi {
@GET("/resource")
String someResource(@Header("x-header") String value);
@GET("/resource")
String someResource(@Header(name = "x-header") String value);
@GET("/resource")
String someResource(@Header("x-header") Collection<String> values); // all collection values will be sent, in a comma-separared list
@GET("/resource")
String someResource(@Header("x-header") String[] values); // all array values will be sent, in a comma-separared list
@GET("/resource")
String someResource(@Header Map<String, String> values); // map keys will be used as header names
@GET("/resource")
String someResource(@Header com.github.ljtfreitas.julian.Headers headers); // Headers is an immutable collection of Header objects
}
Cookies can be defined using @Cookie
annotation (the content will be sent in the Cookie
header):
import com.github.ljtfreitas.julian.contract.GET;
import com.github.ljtfreitas.julian.contract.Cookie;
public interface MyApi {
@GET("/resource")
@Cookie(name = "my-cookie", value = "cookie-value")
String someResource();
@GET("/resource")
@Header(name = "my-cookie", value = {"cookie-value", "another-cookie-value"}) // multiple values are acceptable
@Header(name = "another-cookie", value = "cookie-value") // @Cookie is repeatable
String someResource();
}
import com.github.ljtfreitas.julian.contract.GET;
import com.github.ljtfreitas.julian.contract.Cookie;
@Header(name = "my-cookie", value = "cookie-value") // common cookies can be defined here
public interface MyApi {
@GET("/resource")
String someResource(); // my-cookie will be sent on Cookie header (cookies on top are inherited for all methods)
}
Cookies, of course, can be dynamic as well:
public interface MyApi {
@GET("/resource")
String someResource(@Cookie("my-cookie") String value);
@GET("/resource")
String someResource(@Cookie(name = "my-cookie") String value);
@GET("/resource")
String someResource(@Cookie("my-cookie") Collection<String> values); // all collection values will be sent
@GET("/resource")
String someResource(@Cookie("my-cookie") String[] values); // all array values will be sent
@GET("/resource")
String someResource(@Cookie Map<String, String> values); // map keys will be used as cookie names
@GET("/resource")
String someResource(@Cookie com.github.ljtfreitas.julian.Cookies cookies); // Cookies is an immutable collection of Cookie objects
}
We can send an object to be used as HTTP request body, using the @Body
on the argument:
import com.github.ljtfreitas.julian.contract.Body;
public interface MyApi {
@POST("/resource")
String create(@Body String bodyAsString);
}
There are two details to pay attention here:
- we need to transform the argument value to the content-type format which we want to use
- we need to serialize the content to a binary stream.
These low-level details are all handled by julian-http-client
; the only requirement is to set a Content-Type
header.
This header can be defined using a @Header
annotation or the same @Body
annotation used on body argument.
public interface MyApi {
@POST("/resource")
String create(@Body("text/plain") String bodyAsString); // text/plain will be used as Content-Type
@POST("/resource")
@Header(name = "Content-Type", value = "text/plain") // or explicitly define the header
String create(@Body String bodyAsString);
}
With the Content-Type
in place, we need an instance of HTTPRequestWriter
able to convert the argument value to the desired format; check out the docs about HTTP request body serialization.
In the examples above, we are using the method return to get the HTTP response body; another option is to use a callback style, with the @Callback
annotation:
import com.github.ljtfreitas.julian.contract.Callback;
public interface MyApi {
@GET("/resource")
void get(@Callback Consumer<String> success); // success callback
@GET("/resource")
void get(@Callback Consumer<Throwable> failure); // failure callback
@GET("/resource")
void get(@Callback Consumer<String> success, @Callback Consumer<Throwable> failure); // success or failure callbacks
@GET("/resource")
void get(@Callback BiConsumer<String, Throwable> callback); // success/failure in the same callback
}
The argument must be a Consumer
, parameterized with the expected response type (String
, in the examples above) or a Throwable
(in this case, it will be a failure callback; check the docs about error handling); or a BiConsumer
, parameterized in the same way (expected response type and a Throwable
).
There are some shortcut annotations to define contract details:
import com.github.ljtfreitas.julian.contract.GET;
import com.github.ljtfreitas.julian.contract.Cookie;
public interface MyApi {
@GET("/resource")
@AcceptAll // alias to @Header(name="Accept", value="*/*")
String someResource();
@GET("/resource")
@AcceptJson // alias to @Header(name="Accept", value="application/json")
String someResource();
@GET("/resource")
@AcceptXml // alias to @Header(name="Accept", value="application/xml")
String someResource();
@POST("/resource")
String someResource(@FormUrlEncoded String urlEncodedBody); // alias to @Body("application/x-www-form-urlencoded")
@POST("/resource")
String someResource(@JsonContent String jsonBody); // alias to a @Body("application/json")
@POST("/resource")
String someResource(@MultipartFormData String multipartBody); // alias to @Body("multipart/form-data")
@POST("/resource")
String someResource(@SerializableContent byte[] binaryBody); // alias to @Body("application/octet-stream")
@POST("/resource")
String someResource(@XmlContent String xmlBody); // alias to @Body("application/xml")
@POST("/resource")
String someResource(@TextPlainContent String textBody); // alias to @Body("text/plain")
@GET("/resource")
String someResource(@Authorization String credentials); // alias to @Header(name="Authorization", value={argument value}"); (more details about authentication, see below)
@GET("/resource")
String someResource(@ContentType String contentType, @Body String body); // alias to @Header(name="Content-Type", value={argument value}")
}
Our next step is to build an instance from our interface. We will do that using the ProxyBuilder
object:
@Path("/base/api")
public interface MyApi {
@GET("/resource")
String get();
}
import com.github.ljtfreitas.julian.ProxyBuilder;
MyApi myApi=new ProxyBuilder().build(MyApi.class,"https://my.api.com");
The base URL is optional, in case you prefer to define it on the interface:
@Path("https://my.api.com/base/api")
public interface MyApi {
@GET("/resource")
String get();
}
import com.github.ljtfreitas.julian.ProxyBuilder;
MyApi myApi=new ProxyBuilder().build(MyApi.class);
ProxyBuilder
has several options to customize HTTP client behaviour, add support to additional media types and additional method return types. These options are explained in detail in the rest of this documentation.
- wildcard
- application/form-url-encodec
- application/json
- application/xml
- application/octet-stream
- multipart/form-data
Modern HTTP API's exchange data using a lot of formats, like json, xml, etc. So, we need to be able to serialize/deserialize Java values to/from these formats.
Of course, this is not new; in fact, a lot of libraries do that. julian-http-client
handles this process in a transparent way using the Content-Type
header; for request bodies, Content-Type
is required, in order to say what is the desired format; for response bodies, julian-http-client
uses the Content-Type
header (from response) as well, to know what is the source media type and convert the body to the desired Java type.
The main abstraction around this work is HTTPMessageCodec
. This interface has two specializations, HTTPRequestWriter
and HTTPResponseReader
. Implementations of these types need to say what media types they care about and, also, what Java types they are able to write to or read from.
By default, julian-http-client
provides a few implementations that work with the "wildcard" media type (*/*
), the most generic one. That means any content can be read or write using any specific mime-type ("text/plain", "application/json", etc) but, because these implementations are generic and does not run any special handling about the request/response bodies, there is some limits about what Java types we can use.
By default, these types are supported:
public interface MyApi {
// HTTP request bodies can be sent using these argument types (the Content-Type header is always required):
@POST("/resource")
void get(@Body("text/plain") String bodyAsString);
// using binary types (byte[], InputStream or ByteBuffer) we can send data in any format ("text/plain" is just an example here; in fact, it could be any mime-type)
@POST("/resource")
void POST(@Body("text/plain") byte[] bodyAsBytes);
@POST("/resource")
void POST(@Body("text/plain") InputStream bodyAsStream);
@POST("/resource")
ByteBuffer POST(@Body("text/plain") ByteBuffer bodyAsStream);
}
public interface MyApi {
// HTTP response bodies from any content type can be read using these return types:
@GET("/resource")
String get();
@GET("/resource")
byte[] get();
@GET("/resource")
InputStream get();
@GET("/resource")
ByteBuffer get();
}
This is nice but not so useful, right? These objects are a bit low level and, except for specific use cases, we do not want to transform a byte stream or a string data in a Java object manually or vice-versa.
julian-http-client
is also able to do that (for requests and responses), because we know the source/target content type. With the suitable HTTPMessageCoded
instance in place, we can read/write any content from/to any Java object that we want to.
application/x-www-form-urlencoded
content type describes form data that is sent in a single block in the HTTP message body.
Check out the docs: aplication/form-url-encoded
application/json
is a very common format in HTTP APIs. julian-http-client
supports json using different implementations:
Two implementations are provided to application/xml
:
application/octet-stream
is the default mime-type for binary content (usually it means an "unknown" content or a binary file). For security reasons, be extremely careful to use it.
This codec is not registered by default. If you want to use it, you need to add explicitly:
import com.github.ljtfreitas.julian.ProxyBuilder;
import com.github.ljtfreitas.julian.http.codec.OctetStreamHTTPMessageCodec;
MyApi myApi = new ProxyBuilder()
.codecs()
.add(new OctetStreamHTTPMessageCodec())
.and()
.build(MyApi.class);
The OctetStreamHTTPMessageCodec
codec is able to serialize and deserialize any Serializable value.
multipart/form-data
content type is used to upload files; check out the docs.
As we see above, julian-http-client
attempts to deserialize the HTTP response body to the method return.
interface MyApi {
@GET
String get(); // response body will be deserialized to a String
}
The same logic is applied for any other return type:
interface MyApi {
@GET
MyResponseType get(); // response body will be deserialized to a MyResponseType object
}
But maybe we want to do it in another way. Let's suppose we want to get a CompletableFuture
because we
want to run an async request:
interface MyApi {
@GET
CompletableFuture<MyResponseType> get();
}
Another case: what if we want to handle an empty response using an Optional
?
interface MyApi {
@GET
Optional<MyResponseType> get();
}
Or, maybe we want to get an Optional
inside an async request?
interface MyApi {
@GET
CompletableFuture<Optional<MyResponseType>> get();
}
The examples above are all valid and work as expected. The response still will be deserialized to a MyResponseType
object, despite the method return is a CompletableFuture
or an Optional
.
This works because julian-http-client
handles two concerns here: the deserialization target from response and the method return. Sometimes they will be the same, sometimes will not. And julian-http-client
is able to convert the deserialization target to the method return, using response transformers. It's just kinda an adapter between types.
julian-http-client
registers several transformers, in order to support a big range of method signatures. If none of the registered adapters be able to handle a type, julian-http-client
is going to use it to deserialize the response; in other words, it will assume the deserialization target and the method return type are the same.
By default these types are supported as method return:
import java.util.Collection;
import java.util.Iterable;
import java.util.Iterator;
import java.util.Optional;
import java.io.InputStream;
import java.nio.ByteBuffer;
interface MyApi {
// byte array
@GET("/")
byte[] byteArray();
// InputStream
@GET("/")
InputStream inputStream();
// ByteBuffer
@GET("/")
ByteBuffer byteBuffer();
// scalar/primitive types (int, long, etc) - wrapper types are supported too
// it assumes a text/plain response type
@GET("/")
int scalar();
// String
@GET("/")
String string();
// Collection<T> or subtypes (List<T>, Set<T>, etc)
// it relies on a HTTPResponseReader able to read the response body as a collection of values
@GET("/")
Collection<YourType> collection();
// Iterable<T> and Iterator<T> are supported too
// again, it relies that HTTP response body can be read as a collection
@GET("/")
Iterable<YourType> iterable();
@GET("/")
Iterator<YourType> iterator();
// Optional can be used to handle an empty response
Optional<YourType> optional();
}
julian-http-client
handles async requests in a transparent way; we just need to get an async value as return type.
These async types from Java's API are supported by default:
import java.lang.Runnable;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
interface MyApi {
@POST("/")
Runnable runnable(@Body("text/plain") String bodyAsString);
@GET("/")
Callable<YourType> callable();
@GET("/")
CompletableFuture<YourType> completableFuture();
@GET("/")
Future<YourType> future();
@GET("/")
FutureTask<YourType> futureTask();
}
Of course, we can mix these signatures to handle different needs. An example, the target endpoint could return a JSON array, that we want get as a Collection<SomeType>
inside an async result:
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
interface MyApi {
// it works as you would expect
@GET("/")
CompletableFuture<Collection<YourType>> asyncCollection();
}
julian-http-client
provides a few types that can be useful as well:
import com.github.ljtfreitas.julian.Attempt;
import com.github.ljtfreitas.julian.Headers;
import com.github.ljtfreitas.julian.Lazy;
import com.github.ljtfreitas.julian.Promise;
import com.github.ljtfreitas.julian.Response;
interface MyApi {
// Attempt is a discriminated union that encapsulates a success or a failure (an Exception) value
@GET("/")
Attempt<YourType> attempt();
// Lazy, like the name says, represents a lazy evaluated result (the result is an Attempt value)
@GET("/")
Lazy<YourType> lazy();
// Promise is the main async abstraction from julian-http-client; it's just an wrapper around an async
// evaluated result (success or failure). The default implementation is backed by a CompletableFuture
@GET("/")
Promise<YourType> promise();
// Headers can be used to get headers response
@HEAD("/")
Headers headers();
// Response is a basic representation from a execution response (handling success outcome or failure)
@GET("/")
Response<YourTYpe> response();
}
Next example shows some HTTP-specific types:
import com.github.ljtfreitas.julian.http.HTTPHeaders;
import com.github.ljtfreitas.julian.http.HTTPResponse;
import com.github.ljtfreitas.julian.http.HTTPStatus;
import com.github.ljtfreitas.julian.http.HTTPStatusCode;
interface MyApi {
// HTTPHeaders is a collection from headers; it can be used to get headers response
@HEAD("/")
HTTPHeaders headers();
// HTTPResponse is a full representation from an HTTP response, including body, headers, and status code.
// This object is able to handle fine-grained control about response details including error handling
@GET("/")
HTTPResponse<YourType> httpResponse();
// the HTTP status from response (code and reason)
@GET("/")
HTTPStatus status();
// the HTTP status code from response
@GET("/")
HTTPStatusCode statusCode();
// again, we can get a method return in the way we want to; for example, we could get a HTTPResponse
// in an async way
@GET("/")
Promise<HTTPResponse<YourType>> asyncHttpResponse();
}
All examples above works as expected with callback responses as well:
import com.github.ljtfreitas.julian.contract.Callback;
public interface MyApi {
@GET("/resource")
void get(@Callback Consumer<HTTPResponse<YourType>> success); // Consumer could be parameterized with any supported type
}
julian-http-client
provides additional support to some libraries:
The first important detail to pay attention is all requests are async; julian-http-client
is able to block until the request has been sent and the response has been received when it is required, but all requests will always run in an async, non-blocking way. This is by design.
public interface MyApi {
@GET("/resource")
String get(); // blocks until receive the response
@GET("/resource")
CompletableFuture<String> get(); // for async types (as CompletableFuture) blocking is not required
}
Also, all response transformations run in an async way.
The main abstraction around HTTP requests is HTTP
:
package com.github.ljtfreitas.julian.http.HTTP;
public interface HTTP {
<T> Promise<HTTPResponse<T>> run(HTTPEndpoint request);
}
This object is intended to be some kind of bridge between I/O work and request/response handling; the default implementation uses a second abstraction to run the HTTP request, HTTPClient
:
package com.github.ljtfreitas.julian.http.client;
public interface HTTPClient {
HTTPClientRequest request(HTTPRequestDefinition request);
}
public interface HTTPClientRequest {
Promise<HTTPClientResponse> execute();
}
HTTPClient
and HTTPClientRequest
are responsible to run a HTTP request and get a HTTP response; the default implementation uses HttpClient.
Both HTTP
and HTTPClient
can be overrided, as well default implementations can be configured (request and response handling and specific HTTP client details):
MyApi myApi = new ProxyBuilder()
.http()
.interceptors()
.add(myHTTPRequestInterceptor) // add request interceptors - see details below
.and()
.failure()
.when(HTTPStatusCode.INTERNAL_SERVER_ERROR, (response, expectedJavaType) -> ...) // custom error handling - see details below
.and()
.encoding()
.with("UTF-8") // charset for requests and responses; UTF-8 is the default value
.and()
.client() // HTTP client configurations
.with(myHTTPClientImplementation) // optionally, we can just override the HTTPClient implementation
.configure() // specific configurations for the default HTTP client implementation
.connectionTimeout(/* value in milliseconds */)
.requestTimeout(/* value in milliseconds */)
.charset("")
.proxyAddress(/* an InetSocketAddress object */)
.redirects()
.follow() // follow 302 responses (default)
.() // or, no follow, if you do not want :)
.and()
.ssl()
.context(/* sets a SSLContext; more details: https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html#sslContext(javax.net.ssl.SSLContext) */)
.parameters(/* sets a SSLParameters; more details: https://docs.oracle.com/en/java/javase/11/docs/api/java.net.http/java/net/http/HttpClient.Builder.html#sslParameters(javax.net.ssl.SSLParameters) */)
.and()
.executor(myThreadPool) // custom Executor object used by HttpClient
.and()
.and()
.async()
// custom Executor object used by async operations
.with(myThreadPool)
.and()
.build(MyApi.class, "https://my.api.com");
We can add interceptors
in the HTTP request pipeline in order to change/add details in the request.
We just need to implement a new HTTPRequestInterceptor
object:
import com.github.ljtfreitas.julian.http.HTTPRequestInterceptor;
public class MyHTTPRequestInterceptor implements HTTPRequestInterceptor {
@Override
public <T> Promise<HTTPRequest<T>> intercepts(Promise<HTTPRequest<T>> request) {
// HTTPRequest is an immutable object.
// the "headers" method takes a new Headers object and returns a new HTTPRequest
return request.then(r -> r.headers(r.headers.join("X-My-Custom-Header", "whatever")));
}
}
The intercepts
method receives a Promise<HTTPRequest>
as argument, so the request handling will run in an async way.
Now, we just need to add our interceptor to the proxy:
MyApi myApi = new ProxyBuilder()
.http()
.interceptors()
.add(new MyHTTPRequestInterceptor())
.and()
.and()
.build(MyApi.class, "https://my.api.com");
julian-http-client
provides a built-in interceptor to add an Authorization header:
import com.github.ljtfreitas.julian.http.auth.Authentication;
import com.github.ljtfreitas.julian.http.auth.BasicAuthentication;
import com.github.ljtfreitas.julian.http.auth.HTTPAuthenticationInterceptor;
Authentication basicAuthentication = new BasicAuthentication("user", "password");
MyApi myApi = new ProxyBuilder()
.http()
.interceptors()
.add(new HTTPAuthenticationInterceptor(basicAuthentication))
.and()
.and()
.build(MyApi.class, "https://my.api.com");
HTTPAuthenticationInterceptor
requires an Authentication
object, and julian-http-client
provides BasicAuthentication
and BearerAuthentication
implementations; in case we want to use any other kind of authorization mechanism, we just need to implement a new Authentication
object.
Check out the docs about error handling.
Currently, there is a few additional implementations:
Error handling can be done in different ways. Of course, the simplest one is using a simple try/catch
block:
import com.github.ljtfreitas.julian.http.HTTPException;
import com.github.ljtfreitas.julian.http.HTTPResponseException;
import com.github.ljtfreitas.julian.http.HTTPClientFailureResponseException;
import com.github.ljtfreitas.julian.http.HTTPServerFailureResponseException;
import com.github.ljtfreitas.julian.http.client.HTTPClientException;
import com.github.ljtfreitas.julian.http.codec.HTTPMessageException;
interface MyApi {
@GET("/some-path")
String get();
}
MyApi myApi = new ProxyBuilder().build(MyApi.class, "https://my.api.com");
try {
String response = myApi.get();
} catch (HTTPClientException e) { // http client/network/IO exceptions
} catch (HTTPMessageException e) { // serialization/deserialization related exceptions
/* HTTPMessageException has two sub-exceptions:
- HTTPRequestWriterException (serialization exceptions)
- HTTPResponseReaderException (deserialization exceptions)
*/
} catch (HTTPResponseException e) { // failure responses (4xx or 5xx status codes)
/* HTTPResponseException has two sub-exceptions:
- HTTPClientFailureResponseException (4xx responses)
- HTTPClientFailureResponseException (5xx responses)
Also, HTTPResponseException provides full access to HTTP response.
*/
HTTPStatus status = e.status();
HTTPHeaders headers = e.headers();
String bodyAsString = e.bodyAsString();
byte[] bodyAsBytes = e.bodyAsBytes();
} catch (HTTPClientFailureResponseException e) { // just 4xx responses (client errors)
/* HTTPClientFailureResponseException has several sub-exceptions, for each 4xx error.
for a fine-grained error handling, we can use them instead:
try {
// ...
} catch (HTTPClientFailureResponseException.NotFound e) { // 404 Not Found responses
} catch (HTTPClientFailureResponseException.BadRequest e) { // 400 Bad Request responses
} catch (// any other HTTPClientFailureResponseException subtype) {
}
*/
} catch (HTTPServerFailureResponseException e) { // just 5xx responses (server errors)
/* HTTPServerFailureResponseException has several sub-exceptions, for each 5xx error.
for a fine-grained error handling, we can use them instead:
try {
// ...
} catch (HTTPServerFailureResponseException.InternalServerError e) { // 500 Internal Server Error responses
} catch (HTTPServerFailureResponseException.GatewayTimeout e) { // 504 Gateway Timeout responses
} catch (// any other HTTPServerFailureResponseException subtype) {
}
*/
} catch (HTTPException e) { // the most generic one; it's the parent for all exceptions above
}
An importante note about HTTPResponseException
: HTTP response failures (4xx or 5xx) are not handled as exceptions; instead are just regular responses. julian-http-client
will throw a HTTPResponseException just in case we try to access the response body, because the body just can be deserialized in case of successful responses.
We can use more declarative approaches as well, using different return types:
import com.github.ljtfreitas.julian.http.HTTPResponse;
interface MyApi {
@GET("/some-path")
HTTPResponse<String> get();
}
MyApi myApi = new ProxyBuilder().build(MyApi.class, "https://my.api.com");
HTTPResponse<String> response = myApi.get();
// HTTPResponse.recover functions allow us to build a new response value when a failure happens
String result = response.recover(exception -> /* ... */) // takes a function which receives an exception and build a new value
.recover(exception -> exception instanceof HTTPClientFailureResponseException.NotFound, exception -> /* ... */) // takes a predicate to check agains an exception and a function to build a new value, in case the predicate matches the exception
.recover(HTTPClientFailureResponseException.NotFound.class, exception -> /* ... */ ) // takes a Class to check against the exception and a function to build a new value, in case the exception is compatible with the expected Class argument
.recover(HTTPStatusCode.NOT_FOUND, (status, headers, bodyAsBytes) -> /* ... */); // takes a HTTPStatusCode to check against the status code from response, and a function to build a new value, in case the failure status code is the same as expected
.body() // HTTPResponse.body() returns an Attempt instance (because the expected response body can or cannot be available)
.unsafe() // this method is "unsafe" because will get either get the successful value or throws the original exception.
Attempt
is a type which abstracts over a computation that could get a successful value or an exception; we can use Attempt
as return value and use several methods to mapping over the expected value or recovering from an exception.
import com.github.ljtfreitas.julian.Attempt;
interface MyApi {
@GET("/some-path")
Attempt<String> get();
}
MyApi myApi = new ProxyBuilder().build(MyApi.class, "https://my.api.com");
Attempt<String> response = myApi.get();
// Attempt.recover functions allow us to build a new response value when a failure happens
// Attempt.failure functions allow us to map over a failure; it can be useful to transform the original exception in a more domain-specific one
String result = response.failure(exception -> /* ... */) // takes a function which receives an exception and map to another
.failure(exception -> exception instanceof HTTPResponseException, exception -> /* ... */) // takes a predicate to check agains an exception and a function to map the failure to a new one, in case the predicate matches the exception
.failure(HTTPClientFailureResponseException.NotFound.class, exception -> /* ... */ ) // takes a Class to check against the exception and a function to map the failure to a new one, in case the exception is compatible with the expected Class argument
.recover(exception -> /* ... */) // takes a function which receives an exception and build a new value
.recover(exception -> exception instanceof HTTPResponseException, exception -> /* ... */) // takes a predicate to check agains an exception and a function to build a new value, in case the predicate matches the exception
.recover(HTTPClientFailureResponseException.NotFound.class, exception -> /* ... */ ) // takes a Class to check against the exception and a function to build a new value, in case the exception is compatible with the expected Class argument
.recover(HTTPStatusCode.NOT_FOUND, (status, headers, bodyAsBytes) -> /* ... */); // takes a HTTPStatusCode to check against the status code from response, and a function to build a new value, in case the failure status code is the same as expected
.unsafe() // this method is "unsafe" because will get either get the successful value or throws the original exception.
For async requests, Promise
has recover methods as well:
import com.github.ljtfreitas.julian.http.HTTPResponse;
import com.github.ljtfreitas.julian.http.client.HTTPClientException;
interface MyApi {
@GET("/some-path")
Promise<String> get();
}
MyApi myApi = new ProxyBuilder().build(MyApi.class, "https://my.api.com");
Promise<String> response = myApi.get();
String result = response.recover(exception -> /* ... */) // takes a function which receives an exception and build a new value
.recover(exception -> exception instanceof HTTPClientException, exception -> /* ... */) // takes a predicate to check agains an exception and a function to build a new value, in case the predicate matches the exception
.recover(HTTPClientException.class, exception -> /* ... */ ) // takes a Class to check against the exception and a function to build a new value, in case the exception is compatible with the expected Class argument
.failure(exception -> /* ... */) // takes a function which receives an exception and map to another; it can be useful to transform the original failure in a more domain-specific one
.join() // Promise.join() returns an Attempt instance (because the expected content can or cannot be available); just remember this method will block the async thread!
.unsafe(); // this method is "unsafe" because will get either get the successful value or throws the exception.
julian-http-client
has dedicated supported for Kotlin language. Check out the docs.
Check out the docs about Resilience4j support.
Check out the docs about OpenTracing support.