I have an Spring + CXF application which consumes a Transmission API: Transmission RPC running in another server.
According to Transmission docs, you need to send a token which is generated on the first request. The server then responds with a 409 http code along with a header containing the token. This token should be sent on all subsequent calls:
2.3.1. CSRF Protection Most Transmission RPC servers require a X-Transmission-Session-Id header to be sent with requests, to prevent CSRF attacks. When your request has the wrong id -- such as when you send your first request, or when the server expires the CSRF token -- the Transmission RPC server will return an HTTP 409 error with the right X-Transmission-Session-Id in its own headers. So, the correct way to handle a 409 response is to update your X-Transmission-Session-Id and to resend the previous request.
I was looking for solution either using a CXF filter or interceptor, that basically will handle the 409 response and retry the initial request adding the token header. I'm thinking that clients can persist this token and send it in future calls.
I'm not very familiar with cxf so I was wondering if this can be accomplish and how. Any hint would be helpful.
Thanks!
The HTTP 409 Conflict response status code indicates a request conflict with the current state of the target resource. Conflicts are most likely to occur in response to a PUT request.
HTTP status codes and the error message can give you a clue. In general, a 5xx status code can be retried, a 4xx status code should be checked first, and a 3xx or 2xx code does not need retried.
Spring Retry provides the ability to automatically re-invoke a failed operation. This is helpful when errors may be transient in nature. For example, a momentary network glitch, network outage, server down, or deadlock.
Here spring-retry can be utilized which is now an independent project and no longer part of spring-batch.
As explained here retry callback will help make another call updated with the token header.
Pseudo code / logic in this case would look something like below
RetryTemplate template = new RetryTemplate();
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
/*
* 1. Check if RetryContext contains the token via hasAttribute. If available set the header else proceed
* 2. Call the transmission API
* 3.a. If API responds with 409, read the token
* 3.a.1. Store the token in RetryContext via setAttribute method
* 3.a.2. Throw a custom exception so that retry kicks in
* 3.b. If API response is non 409 handle according to business logic
* 4. Return result
*/
}
});
Make sure to configure the RetryTemplate
with reasonable retry & backoff policies so as to avoid any resource contention / surprises.
Let know in comments in case of any queries / roadblock.
N.B.: RetryContext
's implementation RetryContextSupport
has the hasAttribute
& setAttribute
method inherited from Spring core AttributeAccessor
Assuming you are using Apache CXF JAX RS Client it is easy to do by just creating a custom Runtime Exception and ResponseExceptionMapper
for it. So the idea is to manually convert 409 outcomes to some exception and then handle them correctly (in your case retry the service call).
See following code snipped for fully working example.
@SpringBootApplication
@EnableJaxRsProxyClient
public class SpringBootClientApplication {
// This can e stored somewhere in db or elsewhere
private static String lastToken = "";
public static void main(String[] args) {
SpringApplication.run(SpringBootClientApplication.class, args);
}
@Bean
CommandLineRunner initWebClientRunner(final TransmissionService service) {
return new CommandLineRunner() {
@Override
public void run(String... runArgs) throws Exception {
try {
System.out.println(service.sayHello(1, lastToken));
// catch the TokenExpiredException get the new token and retry
} catch (TokenExpiredException ex) {
lastToken = ex.getNewToken();
System.out.println(service.sayHello(1, lastToken));
}
}
};
}
public static class TokenExpiredException extends RuntimeException {
private String newToken;
public TokenExpiredException(String token) {
newToken = token;
}
public String getNewToken() {
return newToken;
}
}
/**
* This is where the magic is done !!!!
*/
@Provider
public static class TokenExpiredExceptionMapper implements ResponseExceptionMapper<TokenExpiredException> {
@Override
public TokenExpiredException fromResponse(Response r) {
if (r.getStatus() == 409) {
return new TokenExpiredException(r.getHeaderString("X-Transmission-Session-Id"));
}
return null;
}
}
@Path("/post")
public interface TransmissionService {
@GET
@Path("/{a}")
@Produces(MediaType.APPLICATION_JSON_VALUE)
String sayHello(@PathParam("a") Integer a, @HeaderParam("X-Transmission-Session-Id") String sessionId)
throws TokenExpiredException;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With