Enterprise Java

Access Request and Response Body in HandlerInterceptor

In a Spring Boot application, we sometimes need to inspect or log the HTTP request body and response body. This can be useful for debugging, auditing, troubleshooting, or request-response logging. Spring provides the HandlerInterceptor interface, which allows us to execute custom logic before and after a controller method is called. However, directly reading the request body or response body inside an interceptor is not straightforward because HTTP streams are normally consumed during request processing. A simple solution is to use ContentCachingRequestWrapper and ContentCachingResponseWrapper inside a servlet filter. The interceptor can then access the cached request and response content.

1. Accessing Request and Response Bodies in HandlerInterceptor

Suppose we have the following Spring Boot API: HTTP POST /api/users. The client sends user details to the API as JSON in the HTTP request body.

{"name":"John","email":"john@example.com"}

After processing the request, the API returns the result to the client in the HTTP response body.

{"message":"User created successfully"}

We want to log both values from a Spring MVC interceptor:

Request Body: {"name":"John","email":"john@example.com"}

Response Body: {"message":"User created successfully"}

In Spring MVC, a request body is typically read by an HttpMessageConverter when Spring converts JSON into a Java object for an @RequestBody parameter. Since the servlet request input stream is normally read only once, trying to call request.getReader() or request.getInputStream() later inside a HandlerInterceptor may return no content. The response body has a similar limitation. Once the response is written to the servlet output stream, an interceptor does not automatically have a reusable copy of that data. To solve this, Spring provides ContentCachingRequestWrapper and ContentCachingResponseWrapper. These wrappers cache the content while the request is being read and the response is being written.

The wrappers should be applied before the request reaches the Spring MVC interceptor, usually inside a servlet filter. The request then follows this flow: Client → Filter (Wrap request and response) → HandlerInterceptor → Controller → HandlerInterceptor (Read cached content) → Filter (Copy response back to client) → Client. An important point is that ContentCachingRequestWrapper does not read the request body by itself. It only caches the bytes when Spring or another component actually reads them. Therefore, the request body may still be empty in preHandle(), while it is normally available later in the request lifecycle. Using these wrappers allows the controller to process the request normally while giving the interceptor access to a cached copy of both the request and response bodies.

1.1 How HandlerInterceptor Works

A Spring HandlerInterceptor allows us to intercept requests during Spring MVC request processing. The main interceptor methods are:

  • preHandle() – Executes before the controller method. It is commonly used for authentication, validation, or request checks. Returning true continues the request, while false stops it.
  • postHandle() – Executes after the controller method completes but before the response is fully processed. It can be used to perform additional processing on the response.
  • afterCompletion() – Executes after the complete request-response cycle finishes. It is useful for logging, cleanup, and accessing cached request and response data.

1.2 Understanding ContentCachingRequestWrapper and ContentCachingResponseWrapper

Spring provides ContentCachingRequestWrapper and ContentCachingResponseWrapper to cache the content of an HTTP request and response. They wrap the original HttpServletRequest and HttpServletResponse without changing how the controller normally processes them.

  • ContentCachingRequestWrapper caches the request body as it is read by Spring. The cached content can later be retrieved using getContentAsByteArray(). It is important to note that creating the wrapper does not immediately read the request body; the content is cached only when the body is actually consumed.
  • ContentCachingResponseWrapper caches the data written to the HTTP response. This allows the response body to be retrieved using getContentAsByteArray() before it is finally sent to the client.

1.3 Key Points to Consider

This solution is useful for debugging and request-response logging, but there are a few practical points to keep in mind.

  • Avoid logging sensitive information such as passwords, tokens, authorization headers, or payment details.
  • Request and response bodies are stored in memory, so avoid logging very large uploads or downloads.
  • The request cache may be empty inside preHandle() because the controller has not read the request body yet.
  • Always call copyBodyToResponse() when using ContentCachingResponseWrapper.

2. Implementing Request and Response Body Logging

The example contains a simple REST endpoint that accepts a user and returns a success message.

2.1 Define the Request DTO

package com.example.demo.dto;

public class UserRequest {

  private String name;
  private String email;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }
}

The UserRequest class is a simple DTO (Data Transfer Object) used to represent the JSON data received in the request body. It contains the name and email fields along with their getter and setter methods. When this class is used with @RequestBody, Spring automatically converts the incoming JSON request into a UserRequest object.

2.2 Create the REST Controller

package com.example.demo.controller;

import com.example.demo.dto.UserRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation. * ;

import java.util.Map;

@RestController@RequestMapping("/api/users")
public class UserController {

  @PostMapping
  public ResponseEntity < Map < String, String >> createUser(@RequestBody UserRequest request) {
    return ResponseEntity.ok(Map.of("message", "User created successfully"));
  }
}

The UserController class defines a REST API for handling user requests. The @RestController annotation marks the class as a REST controller, while @RequestMapping("/api/users") sets the base URL. The @PostMapping method handles HTTP POST requests, and @RequestBody converts the incoming JSON body into a UserRequest object. Finally, ResponseEntity.ok() returns an HTTP 200 response containing the "User created successfully" message.

2.3 Add the Content Caching Filter

package com.example.demo.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;

@Component
public class ContentCachingFilter extends OncePerRequestFilter {

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

    try {
      filterChain.doFilter(requestWrapper, responseWrapper);
    } finally {
      responseWrapper.copyBodyToResponse();
    }
  }
}

The ContentCachingFilter extends OncePerRequestFilter, ensuring that the filter runs once for each request. Inside doFilterInternal(), the original request and response are wrapped using ContentCachingRequestWrapper and ContentCachingResponseWrapper so their content can be cached and accessed later. The wrapped objects are passed through the filter chain using filterChain.doFilter(). Finally, copyBodyToResponse() copies the cached response body back to the actual response so it can be returned to the client.

2.4 Create the Request-Response Logging Interceptor

package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.nio.charset.StandardCharsets;

@Component
public class RequestResponseLoggingInterceptor implements HandlerInterceptor {

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    if (request instanceof ContentCachingRequestWrapper requestWrapper) {
      byte[] requestContent = requestWrapper.getContentAsByteArray();
      String requestBody = new String(requestContent, StandardCharsets.UTF_8);
      System.out.println("Request Body: " + requestBody);
    }

    if (response instanceof ContentCachingResponseWrapper responseWrapper) {
      byte[] responseContent = responseWrapper.getContentAsByteArray();
      String responseBody = new String(responseContent, StandardCharsets.UTF_8);
      System.out.println("Response Body: " + responseBody);
    }
  }
}

The RequestResponseLoggingInterceptor implements HandlerInterceptor and uses the afterCompletion() method to access the request and response after processing is complete. It first checks whether they are instances of ContentCachingRequestWrapper and ContentCachingResponseWrapper. The cached content is retrieved using getContentAsByteArray(), converted from a byte array to a UTF-8 String, and then printed to the console as the request body and response body.

2.5 Register the Interceptor

package com.example.demo.config;

import com.example.demo.interceptor.RequestResponseLoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  private final RequestResponseLoggingInterceptor interceptor;

  public WebConfig(RequestResponseLoggingInterceptor interceptor) {
    this.interceptor = interceptor;
  }

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(interceptor).addPathPatterns("/api/**");
  }
}

The WebConfig class implements WebMvcConfigurer to customize Spring MVC configuration. The RequestResponseLoggingInterceptor is injected through the constructor and registered inside the addInterceptors() method. The addPathPatterns("/api/**") configuration ensures that the interceptor is applied only to requests whose URLs start with /api/.

2.6 Run the Application and Verify the Output

Run the Spring Boot application and send an HTTP POST request to /api/users with the following JSON request body:

{"name":"John","email":"john@example.com"}

After the request is processed, the API returns the following response:

{"message":"User created successfully"}

The interceptor reads the cached request and response bodies and prints the following output in the application console:

Request Body: {"name":"John","email":"john@example.com"}
Response Body: {"message":"User created successfully"}

3. Conclusion

In this article, we learned how to access the request and response bodies in a Spring HandlerInterceptor. Since the request and response streams cannot be safely read multiple times, we used ContentCachingRequestWrapper and ContentCachingResponseWrapper inside a filter to cache their content. The interceptor can then retrieve the cached data using getContentAsByteArray() after request processing is complete. This approach provides a simple and effective way to log or inspect request and response bodies in Spring Boot applications.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button