5

I have a requirement to Base64 decode every JSON request payload that my Spring Boot service receives. The JSON payload would have been Base64 encoded at the client before posting using the HTTP POST method. Further, I also need to Base64 encode the JSON response before presenting to the calling client application.

I am required to reduce boilerplate code by using handler interceptors. I have already achieved the request/incoming leg of the operation by the use of interceptors but is yet to achieve this for the response leg. I have posted the code snippets below. The code to intercept the response and base64 encode it is in the postHandle method of the interceptor class.

What am I doing wrong here?

Interceptor Class:

public class Base64ResponseEncodingInterceptor implements HandlerInterceptor {   
    private static final String DECODED_REQUEST_ATTRIB = "decodedRequest";
    private static final String POST = "POST";


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView arg3) throws Exception {
          try {
              if (POST.equalsIgnoreCase(request.getMethod())) {
                    CharResponseWrapper res = new CharResponseWrapper(response);
                    res.getWriter();
                    byte[] encoded = Base64.encodeBase64(res.toString().getBytes());
                    byte[] encoded = Base64.encodeBase64(response.getHeader(ENCODED_RESPONSE_ATTRIB).getBytes());
                    response.getWriter().write(new String(encoded));
              }
          } catch (Exception e) {
              throw new Exception(e.getMessage());
          }
    }


    // preHandle and afterCompletion methods
    // Omitted 
}

The CharResponseWrapper Class used above:

public class CharResponseWrapper extends HttpServletResponseWrapper {

    protected CharArrayWriter charWriter;

    protected PrintWriter writer;

    protected boolean getOutputStreamCalled;

    protected boolean getWriterCalled;


    public CharResponseWrapper(HttpServletResponse response) {
        super(response);
        charWriter = new CharArrayWriter();
    }


    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (getWriterCalled) {
            throw new IllegalStateException("getWriter already called");
        }
        getOutputStreamCalled = true;
        return super.getOutputStream();
    }


    @Override
    public PrintWriter getWriter() throws IOException {
        if (writer != null) {
            return writer;
        }
        if (getOutputStreamCalled) {
            throw new IllegalStateException("getOutputStream already called");
        }
        getWriterCalled = true;
        writer = new PrintWriter(charWriter);
        return writer;
    }


    @Override
    public String toString() {
        String s = null;
        if (writer != null) {
            s = charWriter.toString();
        }
        return s;
    }
}

JavaConfig Class where Interceptor is registered:

@Configuration
@EnableJpaRepositories(repositoryBaseClass = BaseRepositoryBean.class, basePackages = "")
@EntityScan(basePackages = { "com.companyname", "com.companyname.productname"})
public class RestConfig extends WebMvcConfigurerAdapter {


      @Override
      public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new Base64ResponseEncodingInterceptor());
      }

}

The Controller Class, where the Interceptor is used (Only the working request leg is shown here):

@Autowired
HttpServletRequest request;

String decodedRequest = null;

@ModelAttribute("decodedRequest")
public void getDecodedParam(){
    decodedRequest = (String) request.getAttribute("decodedRequest");
} 

The code in the postHandle method does not work. It is either the HttpServletResponse is null or I get an exception message:

getOutputStream already called

Update: Work around solution to reading the response directly in the ResponseBodyAdvice In the Controller Class, I added the following:

@RestController
@RequestMapping("/api/ipmanager")
public class IPProfileRestController extends AbstractRestController {

    @Autowired
    HttpServletResponse response;

   String encodedResponse = null;

   @ModelAttribute("encodedResponse")
    public void getEncodedResponse(){
       response.setHeader("encodedResponse", StringUtils.EMPTY);
    } 

    @RequestMapping(value = "/time", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE }, consumes = {
            MediaType.APPLICATION_JSON_VALUE })
    public @ResponseBody String saveAccessClientTime(@RequestBody String ecodedRequest) {

        // Some code here

        String controllerResponse = prettyJson(iPProfileResponse);
        response.setHeader("encodedResponse", controllerResponse);
        return controllerResponse;
    }
}

I have the following in the ResponseBodyAdvice

@ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, 
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        String body1 = StringUtils.EMPTY;
        // Encode the response and return

        List<String> listOfHeaderValues = response.getHeaders().get("encodedResponse");

        body1=new String(Base64.encodeBase64(listOfHeaderValues.get(0).getBytes()));

        return body1;
    }

}
sage
  • 587
  • 1
  • 8
  • 28

2 Answers2

6

As the Spring MVC documentation states:

the postHandle method of HandlerInterceptor is not always ideally suited for use with @ResponseBody and ResponseEntity methods. In such cases an HttpMessageConverter writes to and commits the response before postHandle is called which makes it impossible to change the response, for example to add a header. Instead an application can implement ResponseBodyAdvice and either declare it as an @ControllerAdvice bean or configure it directly on RequestMappingHandlerAdapter.

With that being said:

What am I doing wrong here?

Since the response has been already committed, you can't change it. In order to change the response you should register a ResponseBodyAdvice<T> and put your response encoding logic there:

@ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, 
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> converterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        // Encode the response and return
    }
}
Ali Dehghani
  • 46,221
  • 15
  • 164
  • 151
  • Thank you, Ali. If I annotate with @ControllerAdvice, do I need to register it in the JavaConfig class like for the Interceptor for the request leg? I will quickly test this as soon as I can and revert with the results. Thanks. – sage Oct 10 '16 at 17:55
  • No there is no need for registration, as long as it's scannable – Ali Dehghani Oct 10 '16 at 17:56
  • Hello Ali, I have slotted this in and it will work. I was able to override the expected http response by sending a String object that was printed out in postman as the response. My challenge now, is the best way to access my response payload as string, so that I can base64 encode before presenting it to the client. response.getBody() only returns an OutputStream. Do you have any snippet for retrieving the payload body from ServerHttpResponse? – sage Oct 11 '16 at 08:23
  • http://stackoverflow.com/questions/216894/get-an-outputstream-into-a-string – Ali Dehghani Oct 11 '16 at 08:27
  • I couldn't retrieve the data from the http response using the link above but I used a work around through setting a key in the response headers and assigning the response data to it within the Controller. In the ResponseBodyAdvice, I then read this value and Base64 encoded it. I will update my question above with the solution, and mark your answer as correct but I will still be interested in being able to read the Http response for the data rather than storing it in the response headers. Thank you very much, Ali – sage Oct 11 '16 at 10:42
  • If all your controllers are returning `String`s, then you're better off defining a `ResponseBodyAdvice` instead of `ResponseBodyAdvice` – Ali Dehghani Oct 11 '16 at 12:16
0

If your method is returning ResponseEntity<T> then write this code in postHandle() method of your HandlerInterceptor:

eg. response.addHeader("jwt_token", "kajdlakjd");

it will work!!

Don
  • 3,876
  • 10
  • 47
  • 76