Reading httprequest content from spring exception handler

人盡茶涼 提交于 2019-12-02 19:29:47

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.

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);
        ....
    }
}

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!