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

2

29

I'm trying to resolve some certain parameters of RequestMapping methods, to extract values from request body and validates them and inject them into certain annotated parameters.

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                              NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    // 1, get corresponding input parameter from NativeWebRequest
    // 2, validate
    // 3, type convertion and assemble value to return
    return null;
}

The biggest problem is that I find out that HttpServletRequest(get from NativeWebRequest) cannot read input stream(some parameters are in the request body) more than one time. So how can I retrieve Inputstream/Reader or the request body more than one time?

Indue answered 15/1, 2016 at 4:2 Comment(2)
One solution could be to use a ThreadLocal to store params inside a filter from request and then use them anywhere in your code at any number of times.Euphemism
@SandeepPoonia This may help. But one problem is, if I save the body into the threadlocal(by calling HttpServletRequest.getReader/getInputStream), this will never be called again. Event in the controller layer, I cannot declare "@RequestBody String body"(which may throw an exception by Spring), because Spring cannot read the input stream anymore.Indue
G
41

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.

Garnishment answered 15/1, 2016 at 8:23 Comment(6)
Yes, this is the best solution so far. But the efficiency is truly a big problem. I think i'll have to find a way to restrict the input stream size in case anyone engage in sabotage or something. As for the request body read twice at least, I think it is acceptable since I only accept json formated input strings from the request body which normally less than 1kb/pr.Indue
Complete example is available in this answer: https://mcmap.net/q/88067/-http-servlet-request-lose-params-from-post-body-after-read-it-onceLuby
by the way, ContentCachingRequestWrapper does't do the same thing, it allows to get the data from cache but you have to first read the real data. It's useful for logging but for other things not very well.Melson
I wanted to use it for multipart/form-data but that's also not supported by ContentCachingRequestWrapperAegyptus
An interesting fact that for some reason Spring's ContentCachingRequestWrapper doesn't really "cache" anything (as what I found via trials and errors). On the contrary, a solution referenced in a sister thread by @Stim, worked perfectly - the one that requires you to implement your own MultiReadHttpServletRequest wrapper.Millisecond
I had problems with ContentCachingRequestWrapper when trying to get a body inside a logging filter. Spring MVC then failed to get a request body the second time from an input stream.Hapless
A
0

Why not just set the value that you read the first time as an attribute of the HTTP servlet request? You would then just access this attribute the next times instead of trying to read the value from the HTTP servlet request again:

@Override
public Object resolveArgument(final MethodParameter parameter,
                              final ModelAndViewContainer mavContainer,
                              final NativeWebRequest webRequest,  
                              final WebDataBinderFactory binderFactory) throws Exception {

   HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
   MyContent content = (MyContent) httpServletRequest.getAttribute("MY_ATTRIBUTE_KEY");
   if (content == null) {
      // will be executed only the first time
      content = readContentFromWebRequest(httpServletRequest);
      httpServletRequest.setAttribute("MY_ATTRIBUTE_KEY", content);
   }

   // do what you need to do with the content, the first time and every other times
}

with this solution, no need to introduce a filter and a special type of HTTP servlet request.

Albeit answered 22/6 at 10:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.