Reading Request Body Multiple Times in Java/Spring Boot
Tags: java, springboot, cache, requestbody
Published: 2024-04-02 08:57:19

To read the POST request body, you can use the `getReader` method of `ServletRequest` interface. 

```java
ServletRequest request;

StringBuilder sb = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
   sb.append(line);
}
String requestBody = sb.toString(); // request body as string
```
The catch here is you cannot read the request body multiple times. As the HTTP request wasn't sent multiple times and the network is a serial medium it requires servlet stack to buffer the request body in case you decide to re-read it. Unless you don't care about memory it makes sense to not buffer multiple requests.

Spring provides a `ContentCachingRequestWrapper#getContentAsByteArray()` method to read the body multiple times. This has limitation we can't read the body multiple times using the `getInputStream()` and `getReader()` methods. This class caches the request body by consuming the `InputStream`. If we read the `InputStream` in one of the filters, then other subsequent filters in the filter chain can't read it anymore. Because of this limitation, this approach is not suitable in all situations.

Let's create a class viz `CacheBodyServletInputStream` extending `ServletInputStream` which provides an input stream for reading binary data from a client request. We will provide custom implementation for handling cached `InputStream`.

```java
public class CacheBodyServletInputStream extends ServletInputStream {

    private final InputStream cacheBodyInputStream;

    public CachBodyServletInputStream(byte[] cacheBody) {
        this.cacheBodyInputStream = new ByteArrayInputStream(cacheBody);
    }

    /**
     * Indicates whether InputStream has more data to read or not.
     *
     * @return true when zero bytes available to read
     */
    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * Indicates whether InputStream is ready for reading or not.
     * Since we've already copied InputStream in a byte array, we'll return true to indicate that it's always available.
     *
     * @return true
     */
    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener listener) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int read() throws IOException {
        return cachedBodyInputStream.read();
    }
}
```

Let's create a class viz `CacheBodyHttpServletRequest` that will cache the request body by reading the body from actual `InputStream` and storing it in `byte[]` object. `CacheBodyHttpServletRequest` overrides the `HttpServletRequestWrapper#getInputStream()` to read the raw body and convert it into `CacheBodyServletInputStream`. It also, overrides the `HttpServletRequestWrapper#getReader()` which returns the cached `byte[]` object as `BufferedReader`.

```java
public class CacheBodyHttpServletRequest extends HttpServletRequestWrapper {

    private final byte[] cacheBody;

    public CacheBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cacheBody = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public ServletInputStream getInputStream() {
        return new CacheBodyServletInputStream(this.cacheBody);
    }

    @Override
    public BufferedReader getReader() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cacheBody);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream));
    }
}
```
Finally, let's create a filter that will create an object of the `CacheBodyHttpServletRequest` from the actual request body.

```java
@Component
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class CacheBodyHttpServletFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        CacheBodyHttpServletRequest cacheBodyHttpServletRequest = new CacheBodyHttpServletRequest(request);
        filterChain.doFilter(cacheBodyHttpServletRequest, response);
    }
}
```

Happy Coding :)