Description
org.springframework.http.client.ClientHttpRequestInterceptor
javadoc describes the interceptor as a "chain", and I assumed that the ClientHttpRequestExecution execution
argument was a reference to the next link/interceptor in the chain (or the terminal request execution.)
The above assumption fails if the interceptor makes more than one call to execution.execute(request,body)
. That is, all calls after the first will skip any following interceptors.
Issue Demo Code
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;
public class ClientHttpRequestInterceptor_IssueDemo {
/**
* Example interceptor that induces the issue by calling the ClientHttpRequestExecution#execute method
* more than once.
*/
private static final ClientHttpRequestInterceptor REPEATING_INTERCEPTOR = (request, body, execution) -> {
System.out.println("Request 1...");
execution.execute(request, body);
System.out.println("Request 2...");
execution.execute(request, body);
System.out.println("Request 3...");
return execution.execute(request, body);
};
/**
* Our last interceptor just logs the request being executed.
*/
private static final ClientHttpRequestInterceptor LOG_INTERCEPTOR = (request, body, execution) -> {
System.out.println("Executing request.");
return execution.execute(request, body);
};
public static void main(String[] args) {
RestClient.builder()
.requestInterceptor(REPEATING_INTERCEPTOR)
.requestInterceptor(LOG_INTERCEPTOR)
.build()
.get().uri("https://www.google.com")
.retrieve()
.toEntity(String.class);
/*
Actual:
Request 1...
Executing request.
Request 2...
Request 3...
Expected:
Request 1...
Executing request.
Request 2...
Executing request.
Request 3...
Executing request.
*/
}
}
Root Cause?
I traced the issue down to the org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution
class which is the type being passed into the interceptors as the execution
argument. Here's an annotated snippet of that class showing the issue:
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
private final Iterator<ClientHttpRequestInterceptor> iterator;
public InterceptingRequestExecution() {
this.iterator = interceptors.iterator();
}
@Override
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
else {
... // terminal request execution here
The use of an iterator and iterator.hasNext()
results in a visitor behavioral pattern instead of an execution chain.
My expected behavior for a chain pattern is that any node in the chain has a reference to the next node in the chain and that there is no state outside of the chain arguments. It seems quite odd that calls to execute the next interceptor in the chain has the side-effect of changing the next reference.
Example Use Case
Using a request interceptor, detect a request that results in a 401 Unauthorized
response and replay the request with a refreshed token in the authorization header. For example:
private static final ClientHttpRequestInterceptor AUTH_INTERCEPTOR = (request, body, execution) -> {
// Presume existing token, previously inserted likely from cache, is still valid and allow the request to proceed...
ClientHttpResponse response = execution.execute(request, body);
// if the server responds with a 401 UNAUTHORIZED then refresh the token and try again (just once with expected good token)...
if (response.getStatusCode().value() == 401) {
response.close(); // be kind
request.getHeaders().setBearerAuth(refreshTokenSupplier.get());
response = execution.execute(request, body);
}
return response;
};
Workarounds
- Inject the
RestClient
into the repeating interceptor - If the interceptor had a reference to the otherwise fully configured rest client, it could use it to make follow on requests. Note that those requests would repeat the interceptor chain from the start instead of here forward. It also tightly couples the injected rest client to the interceptor; in fact, that injection would likely need to happen per request which would require the calling code to create an instance of the repeating interceptor (with a reference to the in flight rest client) and add that instance to the rest client (circular references!). This smells. - Don't do it. - Only call execute once, and then we can pretend it is a chain. But this means whatever business logic (like the auth example) was neatly solved in an interceptor now has to move up to the calling code. While we can of course keep our code DRY, this still requires every place where the behavior is desired to shim in a call to wherever that DRY behavior is located.
- Fix it. - The closest accessible class is
InterceptingClientHttpRequestFactory
which is easy enough to replicate and have it use a fixed version ofInterceptingClientHttpRequest
, but ... I don't see a way to set my version intoDefaultRestClient.interceptingRequestFactory
; a lazily set field in a package-private final class. While I could use reflection to overcome the accessibility issues, its a brittle solution that will earn me the stink eye from peers.