The article outlines a method for logging the request body in a Spring Boot application using a custom filter and request wrapper.
Abstract
The article discusses the importance of logging request bodies for auditing purposes in Spring Boot applications, particularly for POST and PUT requests. It explains that this can be achieved by implementing a Filter that intercepts the request and logs its body. The author details the challenges faced when reading the request body, such as the inability to read an InputStream more than once, leading to errors when the body is subsequently accessed by the application. To address this, the article proposes creating a custom HttpServletRequestWrapper that caches the request body, allowing it to be read multiple times without issues. The solution involves overriding the getInputStream() and getReader() methods to provide a consistent stream of data. The article concludes by refactoring the logging filter to use the custom request wrapper, ensuring that request bodies are logged without causing any downstream errors in the application.
Opinions
The author suggests that logging request bodies is a common requirement for auditing in Spring Boot applications.
Reading the request body directly in a filter can lead to issues since an InputStream can only be read once, which the author identifies as a key challenge.
The author advocates for a solution that involves creating a custom HttpServletRequestWrapper to cache and replay the request body, demonstrating a preference for this approach over alternatives.
The article implies that the proposed solution is superior as it avoids the HttpMessageNotReadableException that occurs when the request body is missing after the first read.
The author provides a complete code example, indicating a commitment to practical, actionable guidance for developers.
By offering a cost-effective AI service recommendation, the author expresses a belief in the value of such tools for developers and possibly endorses the service mentioned.
How to log the request body in a Spring Boot application
When you are building a Spring Boot application, it’s very likely that for auditing purposes you want to log the request body for every POST and PUT request made to the application. The best way to do that is by creating a Filter and registering it through the filter chain. It will be responsible for reading the request body and logging it to the standard output.
But before we start implementing it, let’s first analyze how a ServletRequest is processed:
When we talk about processing content in Java — in our case the request body, everything is represented in terms of streams. We can read content from an InputStream and write to an OutputStream. When your application receives a request, there’s an InputStream created in the background, which is then read and the content is de-serialized by Jackson into a specific type. When your application sends a response, the data of the response is written to an OutputStream, which is then processed by some lower-level entity that sends that content over the wire to the client.
In this post, we are only interested in reading that content. We can create a new filter, LogRequestFilter that will read the InputStream of the request, and log the String representation of that.
This is shown in the code below — using the method logPostOrPutRequestBody we are able to read the InputStream from HttpServletRequest and log it to standard output.
If you run the application with this filter registered, and then make a POST or PUT request to any of the registered endpoints, you will be able to see the body of the request logged in the standard output.
However, the returned response is 5xx — Internal Server Error, and if you explore the standard output of the application, you will notice the following exception:
If you are familiar with how reading InputStream works, you probably know the answer to this — after the filter is processed, the request’s input stream will be re-read and then the content will be parsed by Jackson.
But, an input stream can be only read once.
That’s why during the second attempt to read the input stream we see a failure that the request body is missing.
We need a different approach to solve this, and for that, we need to dive deeper into how HttpServletRequest works.
HttpServletRequest
The InputStream that contains the request body is read from the ServletRequest . If you dive deeper into ServletRequest class, you will notice that it has two methods among others:
getReader()
getInputStream()
So, whenever the body needs to be read and de-serialized by Jackson, the getInputStream is called on the ServletRequest object.
For that reason, we need to find a way to pass a wrappedServletRequest forward to the filter chain, which will be able to read the current input stream, cache it, and log the body to standard output, and then return a new InputStream out of those cached bytes from the body every time getInputStream is called.
We can do that by creating a custom class that extends HttpServletRequestWrapper, as shown in the snippet below:
In this new class, we make sure to read the input stream from the request (it’s fine to do that in the constructor), store that in a local variable, and override both getReader() and getInputStream().
Ideally, getReader should return a reader of your implementation of getInputStream(), as shown below:
The final step is to provide your own implementation of getInputStream() that must return a ServletInputStream. You can create a custom class that extends the ServletInputStream and override all these methods, or return an anonymous class in the getInputStream() method directly.
In this example we take the second approach, by first constructing an InputStream from the cached content:
final ByteArrayInputStream byteArrayInputStream = newByteArrayInputStream(bodyInStringFormat.getBytes());
And then returning a new instance of ServletInputStream that knows how to read that stream.
After finalizing the CustomHttpRequestWrapper class, we do a quick refactor to the LogRequestFilter to use that wrapper and forward it alongside the filter chain.
This time, when you make a POST or PUT request to your application, you shouldn’t see any errors. Also, the request body will be logged in the standard output.
Thank you for reading this post, I hope you find it useful.