Search code examples
javaspringspring-mvcexceptionhandler

Reading httprequest content from spring exception handler


I Am using Spring's @ExceptionHandler annotation to catch exceptions in my controllers.

Some requests hold POST data as plain XML string written to the request body, I want to read that data in order to log the exception. The problem is that when i request the inputstream in the exception handler and try to read from it the stream returns -1 (empty).

The exception handler signature is:

@ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff)

Any thoughts? Is there a way to access the request body?

My controller:

@Controller
@RequestMapping("/user/**")
public class UserController {

    static final Logger LOG = LoggerFactory.getLogger(UserController.class);

    @Autowired
    IUserService userService;


    @RequestMapping("/user")
    public ModelAndView getCurrent() {
        return new ModelAndView("user","response", userService.getCurrent());
    }

    @RequestMapping("/user/firstLogin")
    public ModelAndView firstLogin(HttpSession session) {
        userService.logUser(session.getId());
        userService.setOriginalAuthority();
        return new ModelAndView("user","response", userService.getCurrent());
    }


    @RequestMapping("/user/login/failure")
    public ModelAndView loginFailed() {
        LOG.debug("loginFailed()");
        Status status = new Status(-1,"Bad login");
        return new ModelAndView("/user/login/failure", "response",status);
    }

    @RequestMapping("/user/login/unauthorized")
    public ModelAndView unauthorized() {
        LOG.debug("unauthorized()");
        Status status = new Status(-1,"Unauthorized.Please login first.");
        return new ModelAndView("/user/login/unauthorized","response",status);
    }

    @RequestMapping("/user/logout/success")
    public ModelAndView logoutSuccess() {
        LOG.debug("logout()");
        Status status = new Status(0,"Successful logout");
        return new ModelAndView("/user/logout/success", "response",status);

    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.POST)
    public ModelAndView create(@RequestBody UserDTO userDTO, @PathVariable("id") Long id) {
        return new ModelAndView("user", "response", userService.create(userDTO, id));
    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
    public ModelAndView getUserById(@PathVariable("id") Long id) {
        return new ModelAndView("user", "response", userService.getUserById(id));
    }

    @RequestMapping(value = "/user/update/{id}", method = RequestMethod.POST)
    public ModelAndView update(@RequestBody UserDTO userDTO, @PathVariable("id") Long id) {
        return new ModelAndView("user", "response", userService.update(userDTO, id));
    }

    @RequestMapping(value = "/user/all", method = RequestMethod.GET)
    public ModelAndView list() {
        return new ModelAndView("user", "response", userService.list());
    }

    @RequestMapping(value = "/user/allowedAccounts", method = RequestMethod.GET)
    public ModelAndView getAllowedAccounts() {
        return new ModelAndView("user", "response", userService.getAllowedAccounts());
    }

    @RequestMapping(value = "/user/changeAccount/{accountId}", method = RequestMethod.GET)
    public ModelAndView changeAccount(@PathVariable("accountId") Long accountId) {
        Status st = userService.changeAccount(accountId);
        if (st.code != -1) {
            return getCurrent();
        }
        else {
            return new ModelAndView("user", "response", st);
        }
    }
    /*
    @RequestMapping(value = "/user/logout", method = RequestMethod.GET)
    public void perLogout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        userService.setOriginalAuthority();
        response.sendRedirect("/marketplace/user/logout/spring");
    }
     */

    @ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff) {
    Status st = new Status();
    try {
        Writer writer = new StringWriter();
        byte[] buffer = new byte[1024];

        //Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
        InputStream reader = request.getInputStream();
        int n;
        while ((n = reader.read(buffer)) != -1) {
            writer.toString();

        }
        String retval = writer.toString();
        retval = "";
        } catch (IOException e) {

            e.printStackTrace();
        }

        return new ModelAndView("profile", "response", st);
    }
}

Thank you


Solution

  • I've tried your code and I've found some mistakes in the exception handler, when you read from the InputStream:

    Writer writer = new StringWriter();
    byte[] buffer = new byte[1024];
    
    //Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
    InputStream reader = request.getInputStream();
    int n;
    while ((n = reader.read(buffer)) != -1) {
        writer.toString();
    
    }
    String retval = writer.toString();
    retval = "";
    

    I've replaced your code with this one:

    BufferedReader reader = new BufferedReader(new   InputStreamReader(request.getInputStream()));
    String line = "";
    StringBuilder stringBuilder = new StringBuilder();
    while ( (line=reader.readLine()) != null ) {
        stringBuilder.append(line).append("\n");
    }
    
    String retval = stringBuilder.toString();
    

    Then I'm able to read from InputStream in the exception handler, it works! If you can't still read from InputStream, I suggest you to check how you POST xml data to the request body. You should consider that you can consume the Inputstream only one time per request, so I suggest you to check that there isn't any other call to getInputStream(). If you have to call it two or more times you should write a custom HttpServletRequestWrapper like this to make a copy of the request body, so you can read it more times.

    UPDATE
    Your comments has helped me to reproduce the issue. You use the annotation @RequestBody, so it's true that you don't call getInputStream(), but Spring invokes it to retrieve the request's body. Have a look at the class org.springframework.web.bind.annotation.support.HandlerMethodInvoker: if you use @RequestBody this class invokes resolveRequestBody method, and so on... finally you can't read anymore the InputStream from your ServletRequest. If you still want to use both @RequestBody and getInputStream() in your own method, you have to wrap the request to a custom HttpServletRequestWrapper to make a copy of the request body, so you can manually read it more times. This is my wrapper:

    public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
    
        private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class);
        private final String body;
    
        public CustomHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
    
            StringBuilder stringBuilder = new StringBuilder();
            BufferedReader bufferedReader = null;
    
            try {
                InputStream inputStream = request.getInputStream();
                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                    String line = "";
                    while ((line = bufferedReader.readLine()) != null) {
                        stringBuilder.append(line).append("\n");
                    }
                } else {
                    stringBuilder.append("");
                }
            } catch (IOException ex) {
                logger.error("Error reading the request body...");
            } finally {
                if (bufferedReader != null) {
                    try {
                        bufferedReader.close();
                    } catch (IOException ex) {
                        logger.error("Error closing bufferedReader...");
                    }
                }
            }
    
            body = stringBuilder.toString();
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            final StringReader reader = new StringReader(body);
            ServletInputStream inputStream = new ServletInputStream() {
                public int read() throws IOException {
                    return reader.read();
                }
            };
            return inputStream;
        }
    }
    

    Then you should write a simple Filter to wrap the request:

    public class MyFilter implements Filter {
    
        public void init(FilterConfig fc) throws ServletException {
    
        }
    
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            chain.doFilter(new CustomHttpServletRequestWrapper((HttpServletRequest)request), response);
    
        }
    
        public void destroy() {
    
        }
    
    }
    

    Finally, you have to configure your filter in your web.xml:

    <filter>     
        <filter-name>MyFilter</filter-name>   
        <filter-class>test.MyFilter</filter-class>  
    </filter> 
    <filter-mapping>   
        <filter-name>MyFilter</filter-name>   
        <url-pattern>/*</url-pattern>   
    </filter-mapping>
    

    You can fire your filter only for controllers that really needs it, so you should change the url-pattern according to your needs.

    If you need this feature in only one controller, you can also make a copy of the request body in that controller when you receive it through the @RequestBody annotation.