Feign is a library, which makes it easier to implement a http client. Recently more and more people start writing http clients, because they are creating microservices which communicate with http protocol. So there are all sorts of libraries supporting this task like Jersey, Resteasy and others – and there is Feign.
Today I do not want to explain the basic functionality, this is all done on the Readme page itself. Today I want to get into the details of a feature, which becomes more and more important, because in modern distributed systems, you want to have resilient behaviour, which means that you want to design your service in the way, that it can handle unexpected situations without noticing on user’s site. For example an API you are calling is not reachable at the moment, the request times out or the requested resource is not yet available. To solve this issue, you need to apply a retry pattern, so that you increase the chance that the service request is successfull after the first, the second or the nth attempt.
What most developers don’t know, Feign has a default retryer built-in.
Now I show a few code examples, what you can expect from this feature. What I am showing are junit tests with a client mock, so that we are able to stub certain errors and verify, how many retries have been made.
Case 1) Success
no retry needed.
@Test | |
public void testSuccess() throws IOException { | |
when(clientMock.execute(any(Request.class), any(Options.class))).thenReturn( | |
Response.builder().status(200).headers(Collections.<String, Collection<String>>emptyMap()) | |
.build()); | |
final GitHub github = | |
Feign.builder().client(clientMock).decoder(new GsonDecoder()) | |
.target(GitHub.class, "https://api.github.com"); | |
github.contributors("OpenFeign", "feign"); | |
verify(clientMock, times(1)).execute(any(Request.class), any(Options.class)); | |
} |
Case 2) Destination never reachable.
In this case, we can see the Default Retryer working, which ends up doing 5 attempts, but finally the client invocation throws an exception.
@Test | |
public void testDefaultRetryerGivingUp() throws IOException { | |
when(clientMock.execute(any(Request.class), any(Options.class))).thenThrow( | |
new UnknownHostException()); | |
final GitHub github = | |
Feign.builder().client(clientMock).decoder(new GsonDecoder()) | |
.target(GitHub.class, "https://api.github.com"); | |
try { | |
github.contributors("OpenFeign", "feign"); | |
fail("not failing"); | |
} catch (final Exception e) { | |
} finally { | |
verify(clientMock, times(5)).execute(any(Request.class), any(Options.class)); | |
} | |
} |
Case 3) Configure maximal number of attempts
Taking the same error scenario from case 2, this example shows how to configure the retryer to stop trying after the 3rd attempt.
@Test | |
public void testRetryerAttempts() throws IOException { | |
when(clientMock.execute(any(Request.class), any(Options.class))).thenThrow( | |
new UnknownHostException()); | |
final int maxAttempts = 3; | |
final GitHub github = | |
Feign.builder().client(clientMock).decoder(new GsonDecoder()) | |
.retryer(new Retryer.Default(1, 100, maxAttempts)) | |
.target(GitHub.class, "https://api.github.com"); | |
try { | |
github.contributors("OpenFeign", "feign"); | |
fail("not failing"); | |
} catch (final Exception e) { | |
} finally { | |
verify(clientMock, times(maxAttempts)).execute(any(Request.class), any(Options.class)); | |
} | |
} |
Case 4) trigger retrying by error code decoding
For some (restful) services, http status code 409 (conflict) is used to express a wrong state of the target resource, that might change after resubmitting the request. We simulate, that the first retry will lead to a successfull response.
@Test | |
public void testCustomRetryConfigByErrorDecoder() throws IOException { | |
when(clientMock.execute(any(Request.class), any(Options.class))).thenReturn( | |
Response.builder().status(409).headers(Collections.<String, Collection<String>>emptyMap()) | |
.build(), | |
Response.builder().status(200).headers(Collections.<String, Collection<String>>emptyMap()) | |
.build()); | |
class RetryOn409ConflictStatus extends ErrorDecoder.Default { | |
@Override | |
public Exception decode(final String methodKey, final Response response) { | |
if (409 == response.status()) { | |
return new RetryableException("getting conflict and retry", null); | |
} else | |
return super.decode(methodKey, response); | |
} | |
} | |
final GitHub github = | |
Feign.builder().client(clientMock).decoder(new GsonDecoder()) | |
.errorDecoder(new RetryOn409ConflictStatus()) | |
.target(GitHub.class, "https://api.github.com"); | |
github.contributors("OpenFeign", "feign"); | |
verify(clientMock, times(2)).execute(any(Request.class), any(Options.class)); | |
} |
Case 4a) Behavior without error decoder
If no error decoder is configured, no retry is executed by Feign.
@Test | |
public void test409Error() throws IOException { | |
when(clientMock.execute(any(Request.class), any(Options.class))).thenReturn( | |
Response.builder().status(409).headers(Collections.<String, Collection<String>>emptyMap()) | |
.build(), | |
Response.builder().status(200).headers(Collections.<String, Collection<String>>emptyMap()) | |
.build()); | |
final GitHub github = | |
Feign.builder().client(clientMock).decoder(new GsonDecoder()) | |
.target(GitHub.class, "https://api.github.com"); | |
try { | |
github.contributors("OpenFeign", "feign"); | |
fail("not failing"); | |
} catch (final Exception e) { | |
} finally { | |
verify(clientMock, times(1)).execute(any(Request.class), any(Options.class)); | |
} | |
} |
Case 5) Evaluation of Retry-After header
In contrast to the cases 4 and 4a, any response having a Retry-After header, which is a standard header defined in http protocol, the default Feign behavior is to honor this and trigger a retry at the date given.
@Test | |
public void test400ErrorWithRetryAfterHeader() throws IOException { | |
when(clientMock.execute(any(Request.class), any(Options.class))).thenReturn( | |
Response | |
.builder() | |
.status(400) | |
.headers( | |
Collections.singletonMap(Util.RETRY_AFTER, | |
(Collection<String>) Collections.singletonList("1"))).build(), | |
Response.builder().status(200).headers(Collections.<String, Collection<String>>emptyMap()) | |
.build()); | |
final GitHub github = | |
Feign.builder().client(clientMock).decoder(new GsonDecoder()) | |
.target(GitHub.class, "https://api.github.com"); | |
github.contributors("OpenFeign", "feign"); | |
verify(clientMock, times(2)).execute(any(Request.class), any(Options.class)); | |
} |
You can download my example on Github.
Useful write up – thank you! I used this as a model for my own feign-hystrix testing, good clear examples here, nice work.
Explaines a lot.
Thanks Matthias