How to log HTTP Request and Response in Spring Boot
When you are working on complex systems that are built using multiple micro-services, you will need to know the integrations between them, especially when you are accepting user input. I’ve worked most of my career in the banking and payments industry and knowing what was received and what was returned back to the user is not only important for investigating problems, but a requirement for auditing in certain scenarios. And even if you are not legally required, it is extremally useful to know what each request consisted of.
In this article I will show you how to log the HTTP request body and the HTTP response body in Spring and Spring Boot. This is useful when you are building an API and want to keep track of the requests and responses, even if only during the testing phase of the application. This can be done rather easy, without the need to log in each controller.
ContentCachingRequestWrapper and ContentCachingResponseWrapper
I will be showing first the easy way of doing things that, even if it works, may not be ideal in all scenarios.
The problem that most people will encounter is that the input streams that contains the request body and response body are consumed after they are read. This means that you won’t have a body to send to the controller or a body to respond back. Let’s look at the problem first and after that I will show you the solution.
We start by making a HttpLoggingFitler
that we will be using on all requests to log the body.
@Slf4j
@Component
public class HttpLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String body = new String(request.getInputStream().readAllBytes());
log.info("Request {}", body);
filterChain.doFilter(requestWrapper, responseWrapper);
}
}
This works for logging the request, however we consumed the input stream and no longer have the data by the time we reach our controller. As a result, we will receive DefaultHandlerExceptionResolver
with a message similar to this: Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public tech.petrepopescu.requestlogger.data.ResponseDto tech.petrepopescu.requestlogger.controller.TestController.testMethod(tech.petrepopescu.requestlogger.data.RequestDto)]
You may say that it is fine to have the request logged after processing is done by moving the log.info
line after the doFilter()
method. However, by the time we reach that part of the code, the input stream is consumed, and we no longer have access to its content. Similarly, if we read the response body, we will have nothing to send back to the caller.
Spring, however, has a solution. We can wrap our request and response in a ConteantCaching
request and response that caches the data. Both classes offer a getContentAsByteArray
method that we can use to retrieve the data after the input stream was consumed. Enhancing our filter, we now have something like this:
@Slf4j
@Component
public class HttpLoggingFilter 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);
filterChain.doFilter(requestWrapper, responseWrapper);
logResponse(requestWrapper, responseWrapper);
}
private void logResponse(ContentCachingRequestWrapper requestWrapper, ContentCachingResponseWrapper responseWrapper) throws IOException {
log.info("Request {}", new String(requestWrapper.getContentAsByteArray()));
log.info("Response {}", new String(responseWrapper.getContentAsByteArray()));
responseWrapper.copyBodyToResponse();
}
}

As you can see, we now have both the request and the response logged. Don’t forget to call copyBodyToResponse()
on the response wrapper, otherwise you won’t have anything to send back to the caller.
Logging the request at the start
In most cases, this approach is enough. You have access to the body of both the request and the response. There is one small downside in that the request is logged after processing has ended. If you MUST log the request body at the start of the request, before it is being processed by the controller, things become a bit more complicated. There is no out-of-the-box way of doing this.
There is a way, however, but it does add a bit of overhead to your application and, if you are not careful, it can pose serious performance problems. For example, if the request is too big, it may consume a lot of memory. Furthermore, the examples here are just a starting point and should be enhanced further if you plan on using this in production, especially if you expect files as inputs.
The problem we are facing is that ContentCachingRequestWrapper
has access to the body only after the input stream is consumed by the controller handler. However, if we consume the input stream in our filter, we no longer have the body to work with once it reaches the controller. To fix this, we need to copy back the contents to the input stream after we read it. Sadly, we don’t have access to set the input stream again.
That is why, we need to make our own ContentCachingRequestWrapper
.
public class RepeatableContentCachingRequestWrapper extends ContentCachingRequestWrapper {
private SimpleServletInputStream inputStream;
public RepeatableContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() {
return this.inputStream;
}
public String readInputAndDuplicate() throws IOException {
if (inputStream == null) {
byte[] body = super.getInputStream().readAllBytes();
this.inputStream = new SimpleServletInputStream(body);
}
return new String(super.getContentAsByteArray());
}
}
public class SimpleServletInputStream extends ServletInputStream {
private InputStream delegate;
public SimpleServletInputStream(byte[] data) {
this.delegate = new ByteArrayInputStream(data);
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return this.delegate.read();
}
}
Sadly, we will also have to make a new ServletInputStream
class since we don’t have access to any good implementation from the existing Spring code.
Now, in our filter, we can just call the readInputAndDuplicate()
method to get the http request body and log it.
@Slf4j
@Component
public class HttpLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
RepeatableContentCachingRequestWrapper requestWrapper = new RepeatableContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
logRequest(requestWrapper);
filterChain.doFilter(requestWrapper, responseWrapper);
logResponse(responseWrapper);
}
private void logRequest(RepeatableContentCachingRequestWrapper requestWrapper) throws IOException {
String body = requestWrapper.readInputAndDuplicate();
log.info("Request {}", body);
}
private void logResponse(ContentCachingResponseWrapper responseWrapper) throws IOException {
log.info("Response {}", new String(responseWrapper.getContentAsByteArray()));
responseWrapper.copyBodyToResponse();
}
}

Conclusions
While Spring does not log the request and response out of the box, it does offer a convenient way of doing this. The existing implementation allows us to the log the request only after it was already processed, but with a bit of tinkering, we can bypass this limitation. In this article we learned how to log the HTTP Request and the HTTP response in Spring Boot using the ContentCachingRequestWrapper and ContentCachingResponseWrapper and even extend them to allow a bit of extra functionality.
As always, here is the code: