5

In my project, I have a set of api calls which should filtered through certain set of common validation. In that case, I have to intercept the request before it hits the REST controller, read the request body, do the validations and pass it to the controller if the request passes the validations.

Since the HttpServletRequest cannot be deserialized more than once, I used a HttpServletRequestWrapper to make a copy of the actual request. Using the copy it makes, I do the validations.

Following is the configuration class for intercepting the requests.

public class InterceptorConfig extends WebMvcConfigurerAdapter {

     @Autowired     
     CustomInterceptor customInterceptor;

     @Override  
     public void addInterceptors(InterceptorRegistry registry) { 
        registry.addInterceptor(customInterceptor).addPathPatterns("/signup/**");

     }   
}

Here is my preHandle method inside CustomInterceptor class which extends HandlerInterceptorAdaptor

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    ServletRequest copiedRequest = new HttpRequestWrapper(request);

    Map<String, Object> jsonMap = mapper.readValue(copiedRequest.getInputStream(), Map.class);

    if(jsonMap.containsKey("userId")){
        long userId = jsonMap.get("userId");
        MyClass myObject= myAutowiredService.getMyObject(userId);
        if(myObject == null){
            response.setStatus(HttpStatus.SC_NOT_ACCEPTABLE);
            return false;
        } 
        // some more validations which end up returning false if they are met
    }
    return true;
}

This is my HttpRequestWrapper

public class HttpRequestWrapper extends HttpServletRequestWrapper {

    private byte[] requestBody;

    public HttpRequestWrapper(HttpServletRequest request) throws IOException{
        super(request);

        try {
            requestBody = IOUtils.toByteArray(request.getInputStream());
        } catch (IOException ex) {
            requestBody = new byte[0];
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() {

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

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

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

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

}

All set now. Now, when I send a request to any url with the pattern of /signup/**, all the validations are happening fine. However, once the request hits the controller method, error pops out saying the request body is not available.

Required request body is missing: public com.mypackage.myResponseObject com.mypackage.myController.myControllerMethod(com.mypackage.myDTO)

I am struggling to find the reason for this and also a way to overcome the issue. Is there anything I have done wrong in RequestWrapper class? or anything missing?

Help me to sort this thing out.

Thanks!

vigamage
  • 1,975
  • 7
  • 48
  • 74
  • 3
    You aren't reading a copy you are reading the actual request. If you want that to work you would need to do that in a filter and pass the wrapped request along. Currently the request is still consumed. – M. Deinum Aug 16 '17 at 05:45

1 Answers1

3

The Problem seems to be that you are using an Interceptor to read the HttpServletRequest's InputStream and just wrap it in HttpRequestWrapper but the wrapper is never returned.

I think you should use a Filter

public class CustomFilter extends OncePerRequestFilter {
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ServletRequest copiedRequest = new HttpRequestWrapper(request);

        Map<String, Object> jsonMap = mapper.readValue(copiedRequest.getInputStream(), Map.class);

        if(jsonMap.containsKey("userId")){
            long userId = jsonMap.get("userId");
            MyClass myObject= myAutowiredService.getMyObject(userId);
            if(myObject == null){
                response.setStatus(HttpStatus.SC_NOT_ACCEPTABLE);
                //return false;
            } 
            // some more validations which end up returning false if they are met
         }

         filterChain.doFilter(copiedRequest, (ServletResponse) response);
    }
}

And you need to use this Filter in either web.xml or WebApplicationInitializer

shazin
  • 21,379
  • 3
  • 54
  • 71
  • As I understand, wt I am doing currently is, making a copy of the actual request and use that copy in order to do the validations. the last return statement which returns true will pass the actual request to the controller method. – vigamage Aug 16 '17 at 06:07
  • in the answer you posted, wt goes forward is the copied request. isn't it? Isnt there a way to use the copied thing for our validations and the actual request for the controller stuff. – vigamage Aug 16 '17 at 06:10
  • As I as I know that is not possible. Because you read the InputStream of the actual Request and Streams are one way. – shazin Aug 16 '17 at 06:21
  • I implemented it in the way you instructed. Now I am facing the problem of configuring that to be enabled for certain requests only. The url filtering is the issue now – vigamage Aug 16 '17 at 07:05
  • any advices on that? – vigamage Aug 16 '17 at 07:05
  • 2
    Hi, I do as you instructed here but I still get a `Required request body is missing:` in my RestController with @RequestBody – Sapnesh Naik Dec 17 '19 at 10:00