Retour

Mktd#1 Feign Vs Retrofit : 2 Going Further

This article is the second of the series REST clients in Java.

Previous article: MKTD#1 : Getting started


Challenge #2: Going further...

The second challenge aims at solving advanced problems such as:

  • Authentication and session management
  • Errors management via Java Exception
  • Files upload and download

Session management using Cookies

Out of the box, the server provides authentication and session management mechanisms using JWT and Cookies. A new Java interface describes this operation:

public interface AuthenticationApi {

    String login(LoginPassword loginPassword) throws SecurityException;
}

The response HTTP may return a Set-Cookie header to be decoded. This cookie value will then be added to subsequent requests headers sent to other services.

Token based solutions are simpler to put in place using Feign and Retrofit, but in our scenario we are not trying to follow the simplest approach. You can find more information on this topic on this blog.

Errors management

Regarding errors management, we need to return specific errors based on the HTTP code returned by the server. The following piece of code handles the HTTP error to Java exception mapping:

public static RuntimeException decodeError(int status, String message, Supplier<RuntimeException> defaultCase) {
    switch (status) {
        case 404: // Not Found
            return new NoSuchElementException(message);
        case 400: // Bad Request
            return new IllegalArgumentException(message);
        case 401: // Unauthorized
        case 403: // Forbidden
            return new SecurityException(message);
        default:
            return defaultCase.get();
    }
}

Feign

Pro tip: HTTP requests logging

To ease this challenge implementation, it appears to be very handy to log the HTTP requests and responses.

Quick & Dirty way

The quickest way to achieve this is to use a RequestInterceptor called by Feign before an HTTP request gets built and log it via System.out.

Feign.builder()
        .interceptor(System.out::println) // Quick & Dirty debug
        .decoder(new GsonDecoder())
        .encoder(new GsonEncoder())
        .target(MonkeyRaceApi.class, url);

This obviously works only for HTTP requests, to achieve the same for the responses, similar thing has to be added to the decoder.

Using Feign logger

To avoid third-parties libraries dependencies, Feign defines its own Logger. It consists of an abstract class with a single method to implement. The log level needs to be defined to allow log filtering later on (NONE, BASIC, HEADERS, FULL). Here is an example:

Feign.builder()
        .logLevel(Logger.Level.FULL)
        .logger(new Logger() {
            @Override
            protected void log(String configKey, String format, Object... args) {
                System.out.printf("[%s] ", configKey);
                System.out.printf(format, args);
                System.out.println();
            }
        })
        .decoder(new GsonDecoder())
        .encoder(new GsonEncoder())
        .target(MonkeyRaceApi.class, url);

By default, Feign logger class feign.Logger.JavaLogger relies on the JDK logger java.util.logging.Logger but an extension exists to use other loggers such as SLF4J.

Cookie based Authentication

The first step consists in retrieving the cookie generated by the authentication request. To do so, a specific decoder to handle the response headers and store them as cookies is used.

private static String getAuthToken(String url, String login, String password) {
   AuthenticationApi authenticationApi = builder
           .encoder(new GsonEncoder())
           .decoder((response, type) -> handleCookies(response.headers())) // decode cookies
           .target(AuthenticationApi.class, url);
   return authenticationApi.login(new LoginPassword(login, password));
}

The cookie storage is managed by the CookieManager available since Java6.

private static final CookieManager COOKIE_MANAGER = new CookieManager();

The usage of this CookieManager is handled from the method handleCookies as follows:

private static String handleCookies(Map<String, Collection<String>> headers) {
   // From Map<String, Collection<String>> to Map<String, List<String>>
   Map<String, List<String>> h = headers.entrySet().stream()
           .collect(
             toMap(
               Map.Entry::getKey,
               entry -> entry.getValue().stream().collect(toList()))
            );
   try {
       URI uri = URI.create(BASE_URL);image
       COOKIE_MANAGER.put(uri, h);
       return COOKIE_MANAGER.getCookieStore().get(uri).stream() // Stream<HttpCookie>
               .filter(cookie -> "token".equals(cookie.getName()))
               .findFirst() // Optional<HttpCookie>
               .map(HttpCookie::getValue)
               .orElseThrow(() -> new IllegalStateException("Authentication cookie not found"));
   } catch (IOException e) {
       throw new RuntimeException(e);
   }
}

After being stored, this cookie is automatically sent back in subsequent requests. This is achieved by using the RequestInterceptor mechanism that modifies the RequestTemplate. Feign uses this object to construct the HTTP request.

static MonkeyRaceApi buildRaceApi(String url, String login, String password) {
   getAuthToken(url, login, password);
   return builder
           .requestInterceptor(ApiFactory::addCookies) // Inject Cookies
           .decoder(new GsonDecoder())
           .encoder(new GsonEncoder())
           .target(MonkeyRaceApi.class, url);
}

The interceptor behaves as a RequestTemplate consumer:

private static void addCookies(RequestTemplate template) {
   URI uri = URI.create(BASE_URL);
   COOKIE_MANAGER.getCookieStore().get(uri).stream()
           .map(HttpCookie::toString)
           .forEach(cookie -> template.header("Cookie", cookie));
}

With Feign, the cookie based authentication is closed to the Authorization header mechanism often associated to JWT. As it is based on HTTP headers, the same implementation approach using a RequestInterceptor can be used to handle authentication via token. On the other hand, using cookies makes the code more complex and impacts its readability.

We cam also use feign.Target to manage the authentication, see Feign documentation.

Errors management

Feign proposes a specific errors management mechanism out of the box - if an HTTP response code >= 400. This is done using the ErrorDecoder:

static MonkeyRaceApi buildRaceApi(String url, String login, String password) {
   getAuthToken(url, login, password);
   return builder
           .errorDecoder(ApiFactory::decodeError) // Decode errors
           .requestInterceptor(ApiFactory::addCookies) // Inject Cookies
           .decoder(new GsonDecoder())
           .encoder(new GsonEncoder())
           .target(MonkeyRaceApi.class, url);
}
private static Exception decodeError(String methodKey, Response response) {
    return decodeError(response.status(), methodKey,
            () -> FeignException.errorStatus(methodKey, response));
}
Sometimes it can be useful to have a custom management of 404 errors. The Decoder method feign.Feign.Builder#decode404 can be used to define the default behavior.

Upload

File upload can be achieved in two different ways on the REST server:

  • Upload using a formulaire multipart and a request header Content-type set to multipart/form-data.
  • Direct upload with the file content inside the request body and the request header Content-type set to application/octet-stream.

The two approaches can be implemented using the same principle: a specific Decoder. The second option is easier to set up as managing the multipart request body requires the usage of an external API such as Apache Commons FileUpload Other options are also available https://github.com/xxlabaza/feign-form or https://github.com/pcan/feign-client-test to find implementation examples of the multipart approach.

So the second solution is much simpler, the main idea is to handle the specific case of objects of type java.io.InputStream and to delegate other cases to a traditional JSON encoder. To define the HTTP request body, the method feign.RequestTemplate#body(byte[], java.nio.charset.Charset) needs to be called. It is possible to write the encoder body inside a lambda with Java 8, but it would be preferable to extract this implementation in a new class:

public class UploadEncoder implements Encoder {
    private final Encoder delegate;

    public UploadEncoder(Encoder encoder) {
        super();
        delegate = encoder;
    }

    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
        if (InputStream.class.equals(bodyType)) {
            template.header("Content-type", "application/octet-stream");
            InputStream inputStream = InputStream.class.cast(object);
            // InputStream to byte[]
            try (BufferedInputStream bin = new BufferedInputStream(inputStream);
                 ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = bin.read(buffer)) > 0) {
                    bos.write(buffer, 0, bytesRead);
                }
                bos.flush();
                template.body(bos.toByteArray(), StandardCharsets.UTF_8);
            } catch (IOException e) {
                throw new EncodeException("Cannot upload file", e);
            }
        } else {
            delegate.encode(object, bodyType, template);
        }
    }
}
It is obviously possible to simplify this code by using a library to handle to conversion of InputStream into byte[], but it does not hurt to write try with resources from time to time.It would be fair to say that this code may cause issues when working with large files. But in this scenario, we also need to ask the question: is a REST API the right solution to upload large files?

Download

We use feign.Decoder to download files the same way we did for the upload:

public class DownloadDecoder implements Decoder {
    private final Decoder delegate;

    DownloadDecoder(Decoder decoder) {
        super();
        delegate = decoder;
    }

    @Override
    public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
        if (InputStream.class.equals(type)) {
            return response.body().asInputStream();
        }
        return delegate.decode(response, type);
    }
}

Once again, Feign makes this operation quite simple as soon as the encoders/decoders are correctly used.

Summary

Feign makes the handle of HTTP headers really simple, thus simplifying the authentication mechanisms using cookies or tokens.

Errors management with Feign is also trivial which is one of the key benefits over the usage of Retrofit.

Encoding mechanism provides an easy way to handle file upload and more generally to manage all the scenarios of requests serialising. The decoding mechanism allows retrieving the content of a file that is downloaded and more generally deserialising HTTP responses.

Feign comes with a lot of flexibility using the Encoder, Decoder, RequestInterceptor, ... and we can easily solve common issues we face with REST APIs.

Retrofit

Cookie based Authentication

The easiest way to manage authentication with Retrofit is to use the internal of the HTTP client (okHttp3). Similarly to Feign we send an initial request to fetch the cookie and then reuse the same client for all subsequent requests. Another option consists in using different clients for each requests but reusing the same cookieJar.

Here is a first basic implementation to understand the principle of a cookieJar:

client = new OkHttpClient.Builder()
        .cookieJar(
                new CookieJar() {
                    private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();

                        @Override
                        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                            cookieStore.put(url.uri().getAuthority(), cookies);
                        }

                        @Override
                        public List<Cookie> loadForRequest(HttpUrl url) {
                            List<Cookie> cookies = cookieStore.get(url.uri().getAuthority());
                            return cookies != null ? cookies : new ArrayList<Cookie>();
                    }
                });

A more elegant implementation consists in adding the dependency okhttp-urlconnection from OkHttp.

CookieHandler cookieHandler = new CookieManager(
            new PersistentCookieStore(ctx), CookiePolicy.ACCEPT_ALL);

OkHttpClient httpClient = new OkHttpClient.Builder()
            .cookieJar(new JavaNetCookieJar(cookieHandler));

Errors management

There is no, from my point of view, any ideal solution with Retrofit to manage errors the way it is done with Feign. In this example, we use the functionality of the OkHttp interceptor. There is still a limitation, the exceptions returned must be of type RuntimeException.

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(this::authInterceptor);
private Response authInterceptor(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();
    Response response = chain.proceed(request);

    if (!response.isSuccessful()) {
        RuntimeException ex = ApiFactory.decodeError(response.code(), response.message(), () -> null);
        if (ex != null) {
            throw ex;
        }
    }
    return response;
}

Upload

To implement the upload, we have decided to use the method multipart form with the header Content-type set to multipart/form-data Here is the implementation detail:

The method is added to the service

public interface MonkeyService {

    @Multipart
    @POST("monkeys/{id}/photo")
    Call<Photo> sendPhoto(@Path("id") String id, @Part MultipartBody.Part file);
}

We create a temporary file containing the picture content, then we call the method sendPhoto giving it a RequestBody containing the temporary file to then create the MultipartBody passed down to the service.

@Override
public Photo savePhoto(String id, InputStream stream) throws SecurityException, IllegalArgumentException {
    return executeCall(() -> {
        try {
            Path tmp = Files.createTempFile("exo2", "upload");
            Files.copy(stream, tmp, REPLACE_EXISTING);

            RequestBody requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), tmp.toFile());
            MultipartBody.Part body = MultipartBody.Part.createFormData("photo", tmp.toFile().getName(), requestFile);
            return monkeyService.sendPhoto(id, body);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    });
}

Download

Regarding the download, we only need to call our service by retrieving the response body. To do so, Retrofit proposes a generic type ResponseBody to access the raw response.

We start by adding this method to our service:

public interface MonkeyService {
    @GET("monkeys/{id}/photo")
    Call<ResponseBody> downloadPhoto(@Path("id") String id) throws SecurityException, IllegalArgumentException;
  }

then here is the code to fetch the picture:

@Override
public InputStream downloadPhoto(String id) throws SecurityException, IllegalArgumentException {
    try {
        Response<ResponseBody> response = monkeyService.downloadPhoto(id).execute();
        return response.body().byteStream();
    } catch (IOException e) {
        return null;
    }
}

Summary

It is really simple with Retrofit to manage authentication via cookies and tokens as well as the files upload and download.

On the other side, errors management is far less intuitive. Maybe a better approach exists but we haven't found it yet.

Overall Summary

Whether we used Feign or Retrofit, the management of cookies and files upload/download is easily implemented.

We also found that synchronous errors management is better managed with Feign than Retrofit.

Implementations using OkHttp are common to Retrofit and Feign when the client okhttp-client is used in Feign.