1. Introduction

In this article, we’ll explore how to retrieve the response body from a ServletResponse in a Spring Boot filter.

In essence, we’ll define the problem, and then we’ll use a solution that caches the response body to make it available in the Spring Boot filter. Let’s begin.

2. Understanding the Problem

First, let’s understand the problem we are trying to solve.

When working with Spring Boot filters, it is tricky to access the response body from the ServletResponse. This is because the response body is not readily available, as it is written to the output stream after the filter chain has completed its execution.

However, some operations, such as the generation of a hash signature, require the contents of the response body before sending it to the client. Therefore, we need to find a way to read the contents of the body.

3. Using ContentCachingResponseWrapper in a Filter

To overcome the problem defined previously, we’ll create a custom filter and use the ContentCachingResponseWrapper class provided by Spring Framework:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 
  throws IOException, ServletException {
    ContentCachingResponseWrapper responseCacheWrapperObject = 
      new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);
    filterChain.doFilter(servletRequest, responseCacheWrapperObject);
    byte[] responseBody = responseCacheWrapperObject.getContentAsByteArray();
    MessageDigest md5Digest = MessageDigest.getInstance("MD5");
    byte[] md5Hash = md5Digest.digest(responseBody);
    String md5HashString = DatatypeConverter.printHexBinary(md5Hash);
    responseCacheWrapperObject.getResponse().setHeader("Response-Body-MD5", md5HashString);
    // ...
}

In short, the wrapper class allows us to wrap the HttpServletResponse to cache the response body content and call doFilter() to pass on the request to the next filter.

Keep in mind that we must not forget the doFilter() call here. Otherwise, the incoming request won’t go to the next filter in the spring filter chain, and the application won’t process the request as we expected. In fact, not calling doFilter() is a violation of the servlet specification.

Additionally, we must not forget to call the doFilter() with the responseCacheWrapperObject. Otherwise, the response body won’t be cached. In short, ContentCachingResponseWrapper puts the filter between the response output stream and the client making the HTTP request. So, upon creating the response body output stream, which in this case is right after the doFilter() call, the contents are available inside the filter to be processed.

After using the wrapper, the response body is available within the filter using the getContentAsByteArray() method. We use this method to calculate the MD5 hash.

First, we create an MD5 hash of the response body using the MessageDigest class. Second, we convert the byte array to a hexadecimal string. Third, we set the resulting hash string as a header on the response object using the setHeader() method.

If needed, we can convert the byte array to a string and make the body’s contents more explicit.

Finally, it’s crucial to call copyBodyToResponse() before exiting the doFilter() method to copy the updated response body back to the original response:

responseCacheWrapperObject.copyBodyToResponse();

It is crucial to call copyBodyToResponse() before exiting the doFilter() method. Otherwise, the client won’t receive the complete response.

4. Configuring the Filter

Now, we’ll need to add the filter in Spring Boot:

@Bean
public FilterRegistrationBean loggingFilter() {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new MD5Filter());
    return registrationBean;
}

Here, we are configuring creating a FilterRegistrationBean with the implementation of the filter we created previously.

5. Testing the MD5

At last, we can test everything is working as expected using an integration test in Spring:

@Test
void whenExampleApiCallThenResponseHasMd5Header() throws Exception {
    String endpoint = "/api/example";
    String expectedResponse = "Hello, World!";
    String expectedMD5 = getMD5Hash(expectedResponse);

    MvcResult mvcResult = mockMvc.perform(get(endpoint).accept(MediaType.TEXT_PLAIN_VALUE))
      .andExpect(status().isOk())
      .andReturn();

    String md5Header = mvcResult.getResponse()
      .getHeader("Response-Body-MD5");
    assertThat(md5Header).isEqualTo(expectedMD5);
}

Here, we call the /api/example controller, which returns the “Hello, World!” text in the body. We defined the getMD5Hash() method that converts the response into an MD5 similar to what we use in the filter:

private String getMD5Hash(String input) throws NoSuchAlgorithmException {
    MessageDigest md5Digest = MessageDigest.getInstance("MD5");
    byte[] md5Hash = md5Digest.digest(input.getBytes(StandardCharsets.UTF_8));
    return DatatypeConverter.printHexBinary(md5Hash);
}

6. Conclusion

In this article, we learned how to retrieve the response body from a ServletResponse in a Spring Boot filter using the ContentCachingResponseWrapper class. We used this mechanism to show how we can implement the body’s MD5 encoding in the HTTP response headers.

As always, we can find the complete code over on GitHub.