How to Read Request.Getinputstream() Multiple Times

How to read request.getInputStream() multiple times

You probably start consuming the HttpServletRequest using getReader() in :

String ba = getBaId(getBody(httpRequest)); 

Your servlet tries to call getInputStream() on the same request, which is not allowed. What you need to do is use a ServletRequestWrapper to make a copy of the body of the request, so you can read it with multiple methods. I dont have the time to find a complete example right know ... sorry ...

How can I read request body multiple times in Spring 'HandlerMethodArgumentResolver'?

You can add a filter, intercept the current HttpServletRequest and wrap it in a custom HttpServletRequestWrapper. In your custom HttpServletRequestWrapper, you read the request body and cache it and then implement getInputStream and getReader to read from the cached value. Since after wrapping the request, the cached value is always present, you can read the request body multiple times:

@Component
public class CachingRequestBodyFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
MultipleReadHttpRequest wrappedRequest = new MultipleReadHttpRequest(currentRequest);
chain.doFilter(wrappedRequest, servletResponse);
}
}

After this filter, everybody will see the wrappedRequest which has the capability of being read multiple times:

public class MultipleReadHttpRequest extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedContent;

public MultipleReadHttpRequest(HttpServletRequest request) throws IOException {
// Read the request body and populate the cachedContent
}

@Override
public ServletInputStream getInputStream() throws IOException {
// Create input stream from cachedContent
// and return it
}

@Override
public BufferedReader getReader() throws IOException {
// Create a reader from cachedContent
// and return it
}
}

For implementing MultipleReadHttpRequest, you can take a look at ContentCachingRequestWrapper from spring framework which is basically does the same thing.

This approach has its own disadvantages. First of all, it's somewhat inefficient, since for every request, request body is being read at least two times. The other important drawback is if your request body contains 10 GB worth of stream, you read that 10 GB data and even worse bring that into memory for further examination.

Spring reading request body twice

As referenced in this post: How to Log HttpRequest and HttpResponse in a file?, spring provides the AbstractRequestLoggingFilter you can use to log the request.

AbstractRequestLoggingFilter API Docs, found here

How to read a ServletInputStream more than once when you don't have control of the code that reads it a second time

Create a class that extends HttpServletRequestWrapper. This class will cache the contents on the original request's input stream in a temporary file.

public class CachedHttpServletRequest extends HttpServletRequestWrapper
{

public static final String TEMPORARY_FILENAME_PREFIX = "MyPrefix";
public static final String TEMPORARY_FILENAME_SUFFIX = ".cache";

public static final int LEN_BUFFER = 32768; //32 KB

private File m_TemporaryFile;

public CachedHttpServletRequest(HttpServletRequest httpServletRequest, File temporaryFolder)
throws ServletException {

super(httpServletRequest);

try {
//Create a temporary file to hold the contents of the request's input stream
m_TemporaryFile = File.createTempFile(TEMPORARY_FILENAME_PREFIX, null, temporaryFolder);

//Copy the request body to the temporary file
BufferedInputStream is = new BufferedInputStream(super.getInputStream());
FileOutputStream os = new FileOutputStream(m_TemporaryFile);
byte[] buffer = new byte[LEN_BUFFER];
int bytesWritten = 0;
int bytesRead = is.read(buffer);
while(bytesRead != -1) {
os.write(buffer, 0, bytesRead);
bytesWritten += bytesRead;
bytesRead = is.read(buffer);
}
is.close();
os.close();
}
catch(Exception e) {
throw new ServletException(e);
}
}

public void cleanup() {
m_TemporaryFile.delete();
}

@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedServletInputStream(m_TemporaryFile);
}

@Override
public BufferedReader getReader() throws IOException {
String enc = getCharacterEncoding();
if(enc == null) enc = "UTF-8";
return new BufferedReader(new InputStreamReader(getInputStream(), enc));
}
}

Create a class that extends ServletInputStream. Your request wrapper class will return an instance of this custom input stream when getInputStream() or getReader() is called. The custom input stream class will open the cached contents using the temporary file.

public class CachedServletInputStream extends ServletInputStream {

private File m_TemporaryFile;
private InputStream m_InputStream;

public CachedServletInputStream(File temporaryFile) throws IOException {
m_TemporaryFile = temporaryFile;
m_InputStream = null;
}

private InputStream acquireInputStream() throws IOException {
if(m_InputStream == null) {
m_InputStream = new FileInputStream(m_TemporaryFile);
}

return m_InputStream;
}

public void close() throws IOException {
try {
if(m_InputStream != null) {
m_InputStream.close();
}
}
catch(IOException e) {
throw e;
}
finally {
m_InputStream = null;
}
}

public int read() throws IOException {
return acquireInputStream().read();
}

public boolean markSupported() {
return false;
}

public synchronized void mark(int i) {
throw new UnsupportedOperationException("mark not supported");
}

public synchronized void reset() throws IOException {
throw new IOException(new UnsupportedOperationException("reset not supported"));
}
}

Create a class that implements javax.servlet.Filter that instantiates your custom request wrapper when it detects a request that requires cached input stream behavior.

public class CachedHttpServletRequestFilter implements Filter {

public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
public static final String MIME_APPLICATION__X_WWW_FORM_URL_ENCODED = "application/x-www-form-urlencoded";

private File m_TemporaryFolder;

public CachedHttpServletRequestFilter() {
m_TemporaryFolder = new File(/*...your temporary directory goes here...*/);
}

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {

if(servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// Check wether the current request needs to be able to support the body to be read multiple times
String contentType = StringHelper.getLowercaseTrimmed(request.getHeader(HTTP_HEADER_CONTENT_TYPE));

if(contentType.equals(MIME_APPLICATION__X_WWW_FORM_URL_ENCODED)) {
// Override current HttpServletRequest with custom implementation
CachedHttpServletRequest cachedRequest = new CachedHttpServletRequest(request, m_TemporaryFolder);
filterChain.doFilter(cachedRequest, servletResponse);
cachedRequest.cleanup();
return;
}
}

filterChain.doFilter(servletRequest, servletResponse);
}

public void init(FilterConfig filterConfig) throws ServletException {

try {
/* ...initialize where your temporary folder is located here... */
//m_TemporaryFolder = new File(/*...*/);
}
catch(Exception e) {
throw new ServletException(e);
}
}

public void destroy() {
}
}

Http Servlet request lose params from POST body after read it once

As an aside, an alternative way to solve this problem is to not use the filter chain and instead build your own interceptor component, perhaps using aspects, which can operate on the parsed request body. It will also likely be more efficient as you are only converting the request InputStream into your own model object once.

However, I still think it's reasonable to want to read the request body more than once particularly as the request moves through the filter chain. I would typically use filter chains for certain operations that I want to keep at the HTTP layer, decoupled from the service components.

As suggested by Will Hartung I achieved this by extending HttpServletRequestWrapper, consuming the request InputStream and essentially caching the bytes.

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;

public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
}

@Override
public ServletInputStream getInputStream() throws IOException {
if (cachedBytes == null)
cacheInputStream();

return new CachedServletInputStream();
}

@Override
public BufferedReader getReader() throws IOException{
return new BufferedReader(new InputStreamReader(getInputStream()));
}

private void cacheInputStream() throws IOException {
/* Cache the inputstream in order to read it multiple times. For
* convenience, I use apache.commons IOUtils
*/
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}


/* An input stream which reads the cached request body */
private static class CachedServletInputStream extends ServletInputStream {

private final ByteArrayInputStream buffer;

public CachedServletInputStream(byte[] contents) {
this.buffer = new ByteArrayInputStream(contents);
}

@Override
public int read() {
return buffer.read();
}

@Override
public boolean isFinished() {
return buffer.available() == 0;
}

@Override
public boolean isReady() {
return true;
}

@Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
}

Now the request body can be read more than once by wrapping the original request before passing it through the filter chain:

public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

/* wrap the request in order to read the inputstream multiple times */
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);

/* here I read the inputstream and do my thing with it; when I pass the
* wrapped request through the filter chain, the rest of the filters, and
* request handlers may read the cached inputstream
*/
doMyThing(multiReadRequest.getInputStream());
//OR
anotherUsage(multiReadRequest.getReader());
chain.doFilter(multiReadRequest, response);
}
}

This solution will also allow you to read the request body multiple times via the getParameterXXX methods because the underlying call is getInputStream(), which will of course read the cached request InputStream.

Edit

For newer version of ServletInputStream interface. You need to provide implementation of few more methods like isReady, setReadListener etc. Refer this question as provided in comment below.

Reading from the same URL multiple times,not getting the data 2nd time

Calling getInputStream() a second time won't give you a new stream, it will just give you the one that you've already read - and that stream has nothing left to read.

If you really want to read the content twice, I suggest you copy it to a String (or to a byte[] if its content is not text) the first time you read it. You can then read it twice from the String or from the byte[].

Another alternative, if the content is particularly large, would be to copy it off to a temporary file.



Related Topics



Leave a reply



Submit