Amin
Amin

Reputation: 598

How to abort request when using ClientHttpRequestInterceptor in Spring?

If you read the javadoc on ClientHttpRequestInterceptor.intercept method, it says:

execute the request using ClientHttpRequestExecution.execute(org.springframework.http.HttpRequest, byte[]), or do not execute the request to block the execution altogether.

The second part of this statement is what I'm trying to achieve (block the execution altogether based on certain criteria). However, I've not been successful in doing so. I've done my research for few hours and still could not find any examples on how that is done.

I tried returning null and I get NPE later on in the flow as Spring tries to get statusCode from the response (exception below).

java.lang.NullPointerException: null
    at org.springframework.web.client.DefaultResponseErrorHandler.getHttpStatusCode(DefaultResponseErrorHandler.java:56)
    at org.springframework.web.client.DefaultResponseErrorHandler.hasError(DefaultResponseErrorHandler.java:50)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:655)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:620)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:588)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:536)

Edit: Here's a simple interceptor for reference on what I'm trying to achieve:

  public class FailSafeInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
      try{
        return execution.execute(request, body);
      }catch(Throwable e){
        return null;
      }
    }
  }

Possible Solution: after sleeping on the problem, I managed to make it work with the following code (using MockResponse). But I'm not sure if that's the way the doc meant the method to be used. If there's a better way to do this, I'd appreciate the help.

  public class FailSafeInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
      try{
        return execution.execute(request, body);
      }catch(Throwable e){
        return new MockClientHttpResponse(new byte[]{}, HttpStatus.OK);
      }
    }
  }

Upvotes: 1

Views: 3402

Answers (2)

FGreg
FGreg

Reputation: 15330

I was trying to accomplish the same and chose a different route.

In my case, I didn't want to send requests to an external service in our development environment. We are using RestTemplate to interact with the external service (actually, we are using a Spring Integration HTTP Outbound Channel Adapter with a configured RestTemplate). However, I wanted as little configuration to change between environments as possible.

So the solution I came up with was to use a profile-scoped bean so that when the dev profile is selected, it replaces the RestTemplate bean used for external communication with one that never actually executes requests.

Here is my configuration:

package com.example.myproject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.AbstractClientHttpResponse;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

@Configuration
public class InfrastructureConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyApplication.class);

    /**
     * This configuration is used whenever we are running in an environment that
     * does not have access to external endpoints.
     **/
    @Configuration
    @Profile("sit")
    public static class FakeExternalConfig {

        @Bean
        @Qualifier("externalRestTemplate")
        RestTemplate externalRestTemplate() {
            // Returns a Rest Template which will always return HttpStatus.NO_CONTENT without actually executing the request.
            return new RestTemplateBuilder()
                    .additionalInterceptors(((request, body, execution) -> {
                        LOGGER.info("Fake external request sent. Method: {} URI: {} Body: {} Headers: {}", request.getMethodValue(), request.getURI(), new String(body), request.getHeaders().toString());
                        return new AbstractClientHttpResponse() {

                            @Override
                            public HttpHeaders getHeaders() {
                                return new HttpHeaders();
                            }

                            @Override
                            public InputStream getBody() {
                                return new ByteArrayInputStream("" .getBytes(StandardCharsets.UTF_8));
                            }

                            @Override
                            public int getRawStatusCode() {
                                return HttpStatus.NO_CONTENT.value();
                            }

                            @Override
                            public String getStatusText() {
                                return HttpStatus.NO_CONTENT.getReasonPhrase();
                            }

                            @Override
                            public void close() {

                            }
                        };
                    }))
                    .build();
        }

    }

    /**
     * This configuration is used whenever we are running in an environment that
     * does have access to external endpoints.
     **/
    @Configuration
    @Profile({"test", "prod"})
    public static class ExternalConfig {

        @Bean
        @Qualifier("externalRestTemplate")
        RestTemplate externalRestTemplate() {
            return new RestTemplateBuilder()
                    .build();
        }

    }

}

I chose to implement org.springframework.http.client.AbstractClientHttpResponse as an in-line anonymous class because I wanted this to be the only instance where it could be used and there aren't that many methods that need to be overridden. I chose to always return HTTP status 204 No Content because that seemed to make the most sense to me but any other status code could be chosen. (I was very tempted to use I_AM_A_TEAPOT but it is a 4xx status which is considered an error :( )

Upvotes: 0

Ken Chan
Ken Chan

Reputation: 90447

MockClientHttpResponse is from spring-test , it is awkward to include a testing class in the production codes.

If you want to abort sending the whole request once you detect something , just throw RuntimeException (or using the built-in RestClientException):

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

        if(checkNeedAbortSendingRequst()){
           throw new RestClientException("Bla blab bla");
        }
        return execution.execute(request, body);   
    }

Update :

If you want to fail safe , your current solution looks good expect that I would use a non-200 status code if exception is caught (may be 500?). Since I also cannot find a ClientHttpResponse implementation that can be created from scratch or without any external dependencies in spring-mvc , I would copy MockClientHttpResponse from spring-test to the project to use.

Upvotes: 1

Related Questions