Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring - Retry request if service returns 409 HTTP Code

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!

like image 261
David Morabito Avatar asked Jan 18 '17 04:01

David Morabito


People also ask

What is a 409 HTTP response?

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.

Which http codes should retry?

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.

What is spring retry used for?

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.


2 Answers

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

like image 97
Bond - Java Bond Avatar answered Nov 15 '22 18:11

Bond - Java Bond


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;
    }
}
like image 37
Babl Avatar answered Nov 15 '22 18:11

Babl