Reading Request Body Multiple Times in Java/Spring Boot

cynavi

Avinay Basnet

Posted on April 2, 2024

Reading Request Body Multiple Times in Java/Spring Boot

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

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
Enter fullscreen mode Exit fullscreen mode

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.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, let's create a filter that will create an object of the CacheBodyHttpServletRequest from the actual request body.

@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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Happy Coding :)

💖 💪 🙅 🚩
cynavi
Avinay Basnet

Posted on April 2, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related