Reading httprequest content from spring exception handler

后端 未结 3 601
野性不改
野性不改 2021-02-01 23:36

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 req

相关标签:
3条回答
  • 2021-02-01 23:43

    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);
            ....
        }
    }
    
    0 讨论(0)
  • 2021-02-01 23:53

    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 :)

    0 讨论(0)
  • 2021-02-01 23:59

    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.

    0 讨论(0)
提交回复
热议问题