Partner – Payara – NPI (cat=Jakarta EE)
announcement - icon

Can Jakarta EE be used to develop microservices? The answer is a resounding ‘yes’!

>> Demystifying Microservices for Jakarta EE & Java EE Developers

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

Sometimes, we may get the exception IllegalStateException with the error message “getInputStream() has already been called for this request” when we call the getReader() method of the ServletRequest interface in a Java web application.

In this tutorial, we’ll learn why this happens and how to solve it.

2. Problem and Reason

Java Servlet Specification is provided for building web applications in Java. It defines the interfaces ServletRequest/HttpServletRequest with methods getReader() and getInputStream() for reading the data from the HTTP request.

The getReader() method retrieves the body of the request as character data, while getInputStream() retrieves the body of the request as binary data.

The Servlet API documentation for getReader() and getInputStream() emphasizes that they can’t both be used:

public java.io.BufferedReader getReader()
    Either this method or getInputStream may be called to read the body, not both.
    ...
Throws:
    java.lang.IllegalStateException - if getInputStream() method has been called on this request

public ServletInputStream getInputStream()
    Either this method or getReader may be called to read the body, not both.
    ...
    Throws:
    java.lang.IllegalStateException - if the getReader() method has already been called for this request

So with the Tomcat servlet container, when we call getReader() after getInputStream(), we’ll get IllegalStateException: “getInputStream() has already been called for this request”. And when we call getInputStream() after getReader(), we’ll get IllegalStateException: “getReader() has already been called for this request”. 

Here’s a test to reproduce such a situation:

@Test
void shouldThrowIllegalStateExceptionWhenCalling_getReaderAfter_getInputStream() throws IOException {
    HttpServletRequest request = new MockHttpServletRequest();
    try (ServletInputStream ignored = request.getInputStream()) {
        IllegalStateException exception = assertThrows(IllegalStateException.class, request::getReader);
        assertEquals("Cannot call getReader() after getInputStream() has already been called for the current request",
          exception.getMessage());
    }
}

We use MockHttpServletRequest to simulate this situation. We’ll get a similar error message if we call getInputStream() after getReader(). Error messages may vary slightly in different implementations.

3. Use ContentCachingRequestWrapper to Avoid the IllegalStateException

Then how do we avoid such exceptions in our application? A simple way is to avoid invoking them at the same time. But some web frameworks may read the data from the request before our code. If we want to check the input stream more than once, using ContentCachingRequestWrapper provided by the Spring MVC framework is a good choice.

Let’s look at the core part of the ContentCachingRequestWrapper:

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private final ByteArrayOutputStream cachedContent;
    //....
    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (this.inputStream == null) {
            this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
        }
        return this.inputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (this.reader == null) {
            this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
        }
        return this.reader;
    }

    public byte[] getContentAsByteArray() {
        return this.cachedContent.toByteArray();
    }

    //....
}

ContentCachingRequestWrapper wraps the ServletRequest object following the decorator pattern. It overrides its getInputStream() and getReader() methods to not throw IllegalStateException. It also defines a ContentCachingInputStream to wrap the original ServletInputStream to cache the data into an output stream.

After we read the data from the Request object, the ContentCachingInputStream helps us cache the bytes into the cachedContent object of type ByteArrayOutputStream. Then we can read the data repeatedly by calling its getContentAsByteArray() method.

Before we use the ContentCachingRequestWrapper, we need to create a filter to convert the ServletRequest to ContentCachingRequestWrapper:

@WebFilter(urlPatterns = "/*")
public class CacheRequestContentFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (request instanceof HttpServletRequest) {
            String contentType = request.getContentType();
            if (contentType == null || !contentType.contains("multipart/form-data")) {
                request = new ContentCachingRequestWrapper((HttpServletRequest) request);
            }
        }
        chain.doFilter(request, response);
    }
}

Finally, we create a test to make sure it works as expected:

@Test
void givenServletRequest_whenDoFilter_thenCanCallBoth() throws ServletException, IOException {
    MockHttpServletRequest req = new MockHttpServletRequest();
    MockHttpServletResponse res = new MockHttpServletResponse();
    MockFilterChain chain = new MockFilterChain();

    Filter filter = new CacheRequestContentFilter();
    filter.doFilter(req, res, chain);

    ServletRequest request = chain.getRequest();
    assertTrue(request instanceof ContentCachingRequestWrapper);

    // now we can call both getInputStream() and getReader()
    request.getInputStream();
    request.getReader();
}

Actually, there’s a limitation in ContentCachingRequestWrapper that we can’t read the request multiple times. Though we adopt ContentCachingRequestWrapper, we still read bytes from the ServletInputStream of the request object. However, the default ServletInputStream instance doesn’t support reading data more than once. When we reach the end of the stream, calling ServletInputStream.read() will always return -1.

If we want to overcome this limitation, we need to implement the ServletRequest ourselves.

4. Conclusion

In this article, we looked at the documentation of the ServletRequest and understood why we get IllegalStateException. Then, we learned the solution using ContentCachingRequestWrapper provided by the Spring MVC framework.

As usual, all code snippets presented here are available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.