Http Servlet request lose params from POST body after read it once
Solution 1:
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 inputstream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
@Override
public int read() throws IOException {
return input.read();
}
}
}
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.
Solution 2:
I know I'm late, but this question was still relevant for me and this SO post was one of the top hits in Google. I'm going ahead and post my solution in the hopes that someone else might save couple of hours.
In my case I needed to log all requests and responses with their bodies. Using Spring Framework the answer is actually quite simple, just use ContentCachingRequestWrapper and ContentCachingResponseWrapper.
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
chain.doFilter(requestWrapper, responseWrapper);
} finally {
String requestBody = new String(requestWrapper.getContentAsByteArray());
String responseBody = new String(responseWrapper.getContentAsByteArray());
// Do not forget this line after reading response content or actual response will be empty!
responseWrapper.copyBodyToResponse();
// Write request and response body, headers, timestamps etc. to log files
}
}
}
Solution 3:
The above answers were very helpful, but still had some problems in my experience. On tomcat 7 servlet 3.0, the getParamter and getParamterValues also had to be overwritten. The solution here includes both get-query parameters and the post-body. It allows for getting raw-string easily.
Like the other solutions it uses Apache commons-io and Googles Guava.
In this solution the getParameter* methods do not throw IOException but they use super.getInputStream() (to get the body) which may throw IOException. I catch it and throw runtimeException. It is not so nice.
import com.google.common.collect.Iterables;
import com.google.common.collect.ObjectArrays;
import org.apache.commons.io.IOUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.entity.ContentType;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* Purpose of this class is to make getParameter() return post data AND also be able to get entire
* body-string. In native implementation any of those two works, but not both together.
*/
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
public static final String UTF8 = "UTF-8";
public static final Charset UTF8_CHARSET = Charset.forName(UTF8);
private ByteArrayOutputStream cachedBytes;
private Map<String, String[]> parameterMap;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
}
public static void toMap(Iterable<NameValuePair> inputParams, Map<String, String[]> toMap) {
for (NameValuePair e : inputParams) {
String key = e.getName();
String value = e.getValue();
if (toMap.containsKey(key)) {
String[] newValue = ObjectArrays.concat(toMap.get(key), value);
toMap.remove(key);
toMap.put(key, newValue);
} else {
toMap.put(key, new String[]{value});
}
}
}
@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);
}
@Override
public String getParameter(String key) {
Map<String, String[]> parameterMap = getParameterMap();
String[] values = parameterMap.get(key);
return values != null && values.length > 0 ? values[0] : null;
}
@Override
public String[] getParameterValues(String key) {
Map<String, String[]> parameterMap = getParameterMap();
return parameterMap.get(key);
}
@Override
public Map<String, String[]> getParameterMap() {
if (parameterMap == null) {
Map<String, String[]> result = new LinkedHashMap<String, String[]>();
decode(getQueryString(), result);
decode(getPostBodyAsString(), result);
parameterMap = Collections.unmodifiableMap(result);
}
return parameterMap;
}
private void decode(String queryString, Map<String, String[]> result) {
if (queryString != null) toMap(decodeParams(queryString), result);
}
private Iterable<NameValuePair> decodeParams(String body) {
Iterable<NameValuePair> params = URLEncodedUtils.parse(body, UTF8_CHARSET);
try {
String cts = getContentType();
if (cts != null) {
ContentType ct = ContentType.parse(cts);
if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), UTF8_CHARSET);
params = Iterables.concat(params, postParams);
}
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
return params;
}
public String getPostBodyAsString() {
try {
if (cachedBytes == null) cacheInputStream();
return cachedBytes.toString(UTF8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/* An inputStream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
@Override
public int read() throws IOException {
return input.read();
}
}
@Override
public String toString() {
String query = dk.bnr.util.StringUtil.nullToEmpty(getQueryString());
StringBuilder sb = new StringBuilder();
sb.append("URL='").append(getRequestURI()).append(query.isEmpty() ? "" : "?" + query).append("', body='");
sb.append(getPostBodyAsString());
sb.append("'");
return sb.toString();
}
}