Reading httprequest content from spring exception handler
Asked Answered
R

3

15

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

Ribaudo answered 3/3, 2011 at 14:58 Comment(0)
A
11

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.

Autonomic answered 7/3, 2011 at 0:25 Comment(11)
Thank you for your response, I am still not able to read the request body. No exception is thrown, its just that the reader returns null meaning it is empty. Maybe I am missing something regarding the exception handlers in spring. How where you able to read the request?Ribaudo
I've just copied your code (with my corrections) to a new project. I call the controller through a static html form with enctype="multipart/form-data" and an input type="file" to upload a file. I've added a RequestMapping annotation and corresponding method that only throws a RuntimeException. In exception handler method I'm able to log the whole file uploaded through the form. It seems that your inputstream is empty when you enter to the controller or already consumed through another call to getInputStream(). What version of Spring?Autonomic
@Autonomic Spring version 3.0, I do not call getInputStream() explicatively. For example one of the methods that throw an exception is the create() method that receives a UserDTO and saves it to the DB. The exception is thrown in the service and is caught in the controller. Still no new progress :)Ribaudo
I've tested it with Spring 3.0.5. I'd like to help you, but I can't reproduce your issue, so it's very difficult for me. I should see the whole code and spring configuration to understand and also reproduce the problem.Autonomic
@Autonomic - No problem, what of the configuration files would you like to see?Ribaudo
Thanks, first of all your dispatcher servlet configuration.Autonomic
@Autonomic all my configuration is annotation based, config files only hold specific things meaning connection pool or e-mail and security configurationRibaudo
Well. I'd like to reproduce your issue, and surely there are some differences between my project and yours. Take a look here: #4999248 : I was able to reproduce the issue changing two simple lines of my spring configuration. So, I should know as many details as I can to reproduce your problem. ;-)Autonomic
In your reproduction did you use@RequestBody in the controller?Ribaudo
Your comment was the key! I've reproduced your issue, so now you should read my updated answer. ;-)Autonomic
@Autonomic There's no need to write your own wrapper. Spring MVC has ContentCachingRequestWrapper out of the boxTana
V
9

Recently I faced this issue and solved it slightly differently. With spring boot 1.3.5.RELEASE

The filter was implemented using the Spring class ContentCachingRequestWrapper. This wrapper has a method getContentAsByteArray() which can be invoked multiple times.

import org.springframework.web.util.ContentCachingRequestWrapper;
public class RequestBodyCachingFilter implements Filter {

    public void init(FilterConfig fc) throws ServletException {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest)request), response);
    }

    public void destroy() {
    }
}

Added the filter to the chain

@Bean
public RequestBodyCachingFilter requestBodyCachingFilter() {
    log.debug("Registering Request Body Caching filter");
    return new RequestBodyCachingFilter();
}

In the Exception Handler.

@ControllerAdvice(annotations = RestController.class)
public class GlobalExceptionHandlingControllerAdvice {
    private ContentCachingRequestWrapper getUnderlyingCachingRequest(ServletRequest request) {
        if (ContentCachingRequestWrapper.class.isAssignableFrom(request.getClass())) {
            return (ContentCachingRequestWrapper) request;
        }
        if (request instanceof ServletRequestWrapper) {
            return getUnderlyingCachingRequest(((ServletRequestWrapper)request).getRequest());
        }
        return null;
    }

    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(Throwable.class)
    public @ResponseBody Map<String, String> conflict(Throwable exception, HttpServletRequest request) {
        ContentCachingRequestWrapper underlyingCachingRequest = getUnderlyingCachingRequest(request);
        String body = new String(underlyingCachingRequest.getContentAsByteArray(),Charsets.UTF_8);
        ....
    }
}
Vindicable answered 7/10, 2016 at 16:13 Comment(1)
Why is if (request instanceof ServletRequestWrapper) needed?Hundred
L
5

I had the same problem and solved it with HttpServletRequestWrapper as described above and it worked great. But then, I found another solution with extending HttpMessageConverter, in my case that was MappingJackson2HttpMessageConverter.

public class CustomJsonHttpMessageConverter extends  MappingJackson2HttpMessageConverter{

    public static final String REQUEST_BODY_ATTRIBUTE_NAME = "key.to.requestBody";


    @Override
    public Object read(Type type, Class<?> contextClass, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {

        final ByteArrayOutputStream writerStream = new ByteArrayOutputStream();

        HttpInputMessage message = new HttpInputMessage() {
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
            @Override
            public InputStream getBody() throws IOException {
                return new TeeInputStream(inputMessage.getBody(), writerStream);
            }
        };
                    RequestContextHolder.getRequestAttributes().setAttribute(REQUEST_BODY_ATTRIBUTE_NAME, writerStream, RequestAttributes.SCOPE_REQUEST);

        return super.read(type, contextClass, message);
    }

}

com.sun.xml.internal.messaging.saaj.util.TeeInputStream is used.

In spring mvc config

<mvc:annotation-driven >
    <mvc:message-converters>
        <bean class="com.company.remote.rest.util.CustomJsonHttpMessageConverter" />
    </mvc:message-converters>
</mvc:annotation-driven>

In @ExceptionHandler method

@ExceptionHandler(Exception.class)
public ResponseEntity<RestError> handleException(Exception e, HttpServletRequest httpRequest) {

    RestError error = new RestError();
    error.setErrorCode(ErrorCodes.UNKNOWN_ERROR.getErrorCode());
    error.setDescription(ErrorCodes.UNKNOWN_ERROR.getDescription());
    error.setDescription(e.getMessage());


    logRestException(httpRequest, e);

    ResponseEntity<RestError> responseEntity = new ResponseEntity<RestError>(error,HttpStatus.INTERNAL_SERVER_ERROR);
    return responseEntity;
}

private void logRestException(HttpServletRequest request, Exception ex) {
    StringWriter sb = new StringWriter();
    sb.append("Rest Error \n");
    sb.append("\nRequest Path");
    sb.append("\n----------------------------------------------------------------\n");
    sb.append(request.getRequestURL());
    sb.append("\n----------------------------------------------------------------\n");
Object requestBody = request.getAttribute(CustomJsonHttpMessageConverter.REQUEST_BODY_ATTRIBUTE_NAME);

    if(requestBody != null) { 
        sb.append("\nRequest Body\n");
        sb.append("----------------------------------------------------------------\n");
        sb.append(requestBody.toString());

        sb.append("\n----------------------------------------------------------------\n");
    }

    LOG.error(sb.toString());
}

I hope it helps :)

Litchi answered 9/2, 2013 at 11:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.