How to Read Request Body Multiple Times in Spring 'Handlermethodargumentresolver'

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.

Why can the body of the HttpServletRequest not be read multiple times?

Why can the body of the HttpServletRequest not be read multiple times?

Because that would require the servlet stack to buffer the entire request body ... in case the servlet decides to re-read it. That is going to be a performance and/or memory utilization hit for all requests. And it won't work unless there is enough memory to buffer multiple instances (for multiple simultaneous requests) of the largest anticipated request body.

Note that there is no way to get the client to resend the data, short of failing the HTTP request. Even then, the client may not be able to resend it ... because it may not have been able to buffer the data itself.

In short: rereading the request body is not supported by the servlet APIs because it doesn't scale. If a servlet wants to reread the data, it needs to buffer it itself.

Passing multiple variables in @RequestBody to a Spring MVC controller using Ajax

You are correct, @RequestBody annotated parameter is expected to hold the entire body of the request and bind to one object, so you essentially will have to go with your options.

If you absolutely want your approach, there is a custom implementation that you can do though:

Say this is your json:

{
"str1": "test one",
"str2": "two test"
}

and you want to bind it to the two params here:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
public boolean getTest(String str1, String str2)

First define a custom annotation, say @JsonArg, with the JSON path like path to the information that you want:

public boolean getTest(@JsonArg("/str1") String str1, @JsonArg("/str2") String str2)

Now write a Custom HandlerMethodArgumentResolver which uses the JsonPath defined above to resolve the actual argument:

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.jayway.jsonpath.JsonPath;

public class JsonPathArgumentResolver implements HandlerMethodArgumentResolver{

private static final String JSONBODYATTRIBUTE = "JSON_REQUEST_BODY";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonArg.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String body = getRequestBody(webRequest);
String val = JsonPath.read(body, parameter.getMethodAnnotation(JsonArg.class).value());
return val;
}

private String getRequestBody(NativeWebRequest webRequest){
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String jsonBody = (String) servletRequest.getAttribute(JSONBODYATTRIBUTE);
if (jsonBody==null){
try {
String body = IOUtils.toString(servletRequest.getInputStream());
servletRequest.setAttribute(JSONBODYATTRIBUTE, body);
return body;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return "";

}
}

Now just register this with Spring MVC. A bit involved, but this should work cleanly.

Spring Security: deserialize request body twice (oauth2 processing)

So I ended up finding yet another question that addressed this issue that I didn't see before posting (How can I read request body multiple times in Spring 'HandlerMethodArgumentResolver'?). That one also suggested creating a decorator around the HttpServletRequest, so I adapted the info from http://www.myjavarecipes.com/how-to-read-post-request-data-twice-in-spring/, adding a protection against large requests.

Here's what I came up with, in case anyone has any feedback:

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
// We include a max byte size to protect against malicious requests, since this all has to be read into memory
public static final Integer MAX_BYTE_SIZE = 1_048_576; // 1 MB

private String _body;

public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
_body = "";

InputStream bounded = new BoundedInputStream(request.getInputStream(), MAX_BYTE_SIZE);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bounded));

String line;
while ((line = bufferedReader.readLine()) != null){
_body += line;
}
}

@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());

return new ServletInputStream() {
public int read() throws IOException {
return byteArrayInputStream.read();
}

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

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

@Override
public void setReadListener(ReadListener readListener) {

}
};
}

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

I used the following configuration:

    @Bean
FilterRegistrationBean multiReadFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
MultiReadRequestFilter multiReadRequestFilter = new MultiReadRequestFilter();
registrationBean.setFilter(multiReadRequestFilter);
registrationBean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER - 2);
registrationBean.setUrlPatterns(Sets.newHashSet("/path/here"));
return registrationBean;
}

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.

log request body string in RestController ExceptionHandler

  1. Create a filter that wraps your request in a ContentCachingRequestWrapper (nothing more nothing less).

  2. Use the HttpServletRequest as a parameter in your exception handling method as an argument

  3. Check if instance of ContentCachingRequestWrapper

  4. Use the getContentAsByteArray to get the content.

Something like this.

public class CachingFilter extends OncePerRequestFilter {

protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

filterChain.doFilter(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response));
}

NOTE: I wrapped the response as well, just in case you wanted that as well.

Now in your exception handling method use the HttpServletRequest as an argument and use that to your advantage.

@ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
if (req instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) req;
log.error(new String(wrapper.getContentAsByteArray()));
}
return "greeting from @ExceptionHandler";
}

It could be that multiple filters add a wrapper to the HttpServletRequest so you might need to iterate over those wrappers, you could also use this

private Optional<ContentCachingRequestWrapper> findWrapper(ServletRequest req) {
ServletRequest reqToUse = req;
while (reqToUse instanceof ServletRequestWrapper) {
if (reqToUse instanceof ContentCachingRequestWrapper) {
return Optional.of((ContentCachingRequestWrapper) reqToUse);
}
reqToUse = ((ServletRequestWrapper) reqToUse).getRequest();
}
return Optional.empty();
}

Your exception handler would then look something like this

@ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
Optional<ContentCachingRequestWrapper) wrapper = findWrapper(req);
wrapper.ifPresent(it -> log.error(new String(it.getContentAsByteArray())));

return "greeting from @ExceptionHandler";
}

But that might depend on your filter order and if there are multiple filters adding wrappers.

Using @RequestBody and forwarding to another endpoint throws exception Stream closed

The request body object is a stream which can be read only once. So forwarding it is not very trivial. One way around this is to create a filter which reads the input steam and replace the input stream to something which can be read multiple times. Example can be found at another answer:

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

As for your method, there is also another problem:

public void signup(@RequestBody RequestBody requestBody)

As far as I know, RequestBody is an annotation and you can't map it like that. But to get the raw data, you can map it as String.

public void signup(@RequestBody String requestBody)

And then you can just manually make an REST call to the api you want to forward it to using the String request body. Just make sure you set the content-type as the original one, which I assume in this case would be JSON.



Related Topics



Leave a reply



Submit