Read response body in async request - java

I am wondering how to read response in filter from request body if #Controller method returns Callable interface.
My filter looks like this. Response is always empty. Any solution to this? Is this allowed only using AsyncListener?
#Component
public class ResposeBodyXmlValidator extends OncePerRequestFilter {
private final XmlUtils xmlUtils;
private final Resource xsdResource;
public ResposeBodyXmlValidator(
XmlUtils xmlUtils,
#Value("classpath:xsd/some.xsd") Resource xsdResource
) {
this.xmlUtils = xmlUtils;
this.xsdResource = xsdResource;
}
#Override
protected void doFilterInternal(
HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain
) throws ServletException, IOException {
ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(httpServletResponse);
doFilter(httpServletRequest, response, filterChain);
if (MediaType.APPLICATION_XML.getType().equals(response.getContentType())) {
try {
xmlUtils.validate(new String(response.getContentAsByteArray(), response.getCharacterEncoding()), xsdResource.getInputStream());
} catch (IOException | SAXException e) {
String exceptionString = String.format("Chyba při volání %s\nNevalidní výstupní XML: %s",
httpServletRequest.getRemoteAddr(),
e.getMessage());
response.setContentType(MediaType.TEXT_PLAIN_VALUE + "; charset=UTF-8");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().print(exceptionString);
}
}
response.copyBodyToResponse(); // I found this needs to be added at the end of the filter
}
}

The problem of Callable is that the dispatcher servlet itself starts async processing and the filter is exited before actually processing of a request.
When Callable arrives to dispatcher servlet, it frees container thread from pool by releasing all filters (filters basically finish their work). When Callable produces results, the dispatcher servlet is called again with the same request and the response is immidiately fulfilled by the data return from Callable. This is handled by request attribute of type AsyncTaskManager which holds some information about processing of async request. This can be tested with Filter and HandlerInterceptor. Filter is executed only once but HandlerInterceptor is executed twice (original request and the request after Callable completes its job)
When you need to read request and response, one of the solution is to rewrite dispatcherServlet like this:
#Bean
#Primary
public DispatcherServlet dispatcherServlet(WebApplicationContext context) {
return new DispatcherServlet(context) {
#Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
super.service(requestWrapper, responseWrapper);
responseWrapper.copyBodyToResponse();
}
};
}
This way you ensure that you can read request and response multiple times. Other thing is to add HandlerInterceptor like this (you have to pass some data as request attribute):
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
Exception {
Object asyncRequestData = request.getAttribute(LOGGER_FILTER_ATTRIBUTE);
if (asyncRequestData == null) {
request.setAttribute(LOGGER_FILTER_ATTRIBUTE, new AsyncRequestData(request));
}
return true;
}
#Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex
) throws Exception {
Object asyncRequestData = request.getAttribute(LOGGER_FILTER_ATTRIBUTE);
if (asyncRequestData != null && response instanceof ContentCachingResponseWrapper) {
log(request, (ContentCachingResponseWrapper) response, (AsyncRequestData) asyncRequestData);
}
}
afterCompletion method is called only once after async request has been completely processed. preHandle is called exactly twice so you have to check existance of your attribute. In afterCompletion, the response from the call is already present and if you do want to replace it, you should call response.resetBuffer().
This is one possible solution and there could be better ways.

Related

How to log a username in Spring MVC POST request

My goal is to log the incoming http requests to my Spring (5.0.7) MVC Web / Spring security (4.2.3) application. I want to save the requestdata in a database, containing IP, request method, headers, body and the URL. The critical requests are the login attempts so I need to fetch the POST request to the /login URL.
Therefore I wrote a filter to get this done because an interceptor is applied after the filter chain.
I looked at the solution at this SO question and I also tried the variant with an interceptor.
WebAppInitializer
public class WebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
...
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
...
filterRegistration = servletContext.addFilter("logFilter", new APILoggingFilter() );
String[] mappings = new String[] {"/login", "/logout", "/data"};
filterRegistration.addMappingForUrlPatterns(null, false, mappings);
}
}
LoggingFilter
public class APILoggingFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestToUse = request;
HttpServletResponse responseToUse = response;
if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestToUse = new ContentCachingRequestWrapper(request);
}
if (isFirstRequest && !(response instanceof ContentCachingResponseWrapper)) {
responseToUse = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(requestToUse, responseToUse);
String user = SecurityContextHolder.getContext().getAuthentication().getName();
// This is were the logging to the database should take place
if (!isAsyncStarted(request)) {
ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(responseToUse, ContentCachingResponseWrapper.class);
responseWrapper.copyBodyToResponse();
}
}
#Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
}
log4j.properties
log4j.logger.org.springframework=INFO
log4j.logger.org.springframework.web.filter=DEBUG
With this code I am able to log all request to the database with almost all the data I wanted. I see GET requests and POST requests.
The problem or question is: why is it not possible to see the username? I tried to get the username via
request.getRemoteUser();
and with
String user = SecurityContextHolder.getContext().getAuthentication().getName();
It is always null. And here is the curious thing. If I disable the second entry in my log4j.properties (log4j.logger.org.springframework.web.filter=DEBUG) then I always get the username with both options BUT I never fetch a POST request anymore only GET requests.
How do I achieve both goals? Fetch all requests AND get the username?

log request body string in RestController ExceptionHandler

the final goal:
log request body string in RestController's #ExceptionHandler.
explanations
By default, when request is invalid json, springboot throws a HttpMessageNotReadableException, but the message is very generic, and not including specific request body. This makes investigating hard. On the other hand, I can log every request string using Filters, but this way logs will be flooded with too many success ones. I only want to log the request when it is invalid. What I really want is in #ExceptionHandler I'll get that string(previously got somewhere) and log as ERROR.
To illustrate the problem, I created a demo project in github.
the controller:
#RestController
public class GreetController {
protected static final Logger log = LogManager.getLogger();
#PostMapping("/")
public String greet(#RequestBody final WelcomeMessage msg) {
// if controller successfully returned (valid request),
// then don't want any request body logged
return "Hello " + msg.from;
}
#ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e) {
// this is what I really want!
log.error("{the request body string got somewhere, such as Filters }");
return "greeting from #ExceptionHandler";
}
}
the client
valid request
curl -H "Content-Type: application/json" http://localhost:8080 --data '{"from":"jim","message":"nice to meet you!"}'
invalid request(invalid json)
curl -H "Content-Type: application/json" http://localhost:8080 --data '{"from":"jim","message""nice to meet you!"}'
I once tried HandlerInterceptor but will get some error like
'java.lang.IllegalStateException: Cannot call getInputStream() after
getReader() has already been called for the current request'.
after some searching 1 2, I decided to use Filter with ContentCachingRequestWrapper.
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(httpServletRequest);
chain.doFilter(cachedRequest, response);
String requestBody = IOUtils.toString(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding());
log.info(requestBody);
}
This code works well except that the log is after the RestController. if I change the order:
String requestBody = IOUtils.toString(cachedRequest.getReader());
log.info(requestBody);
chain.doFilter(cachedRequest, response);
Works for invalid request, but when request is valid, got following exception:
com.example.demo.GreetController : Required request body is
missing: public java.lang.String
com.example.demo.GreetController.greet(com.example.demo.WelcomeMessage)
I also tried getContentAsByteArray, getInputStream and getReader methods since some tutorials say the framework checks for specific method call.
Tried CommonsRequestLoggingFilter as suggested by #M. Deinum.
But all in vain.
Now I'm bit confused. Can anyone explain the executing order of RestController and Filter, when request is valid and invalid?
Is there any easier way(less code) to achive my ultimate goal? thanks!
I'm using springboot 2.6.3, jdk11.
Create a filter that wraps your request in a ContentCachingRequestWrapper (nothing more nothing less).
Use the HttpServletRequest as a parameter in your exception handling method as an argument
Check if instance of ContentCachingRequestWrapper
Use the getContentAsByteArray to get the content.
Something like this.
public class CachingFilter extends OncePerRequestFilter {
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
filterChain.doFilter(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response));
}
NOTE: I wrapped the response as well, just in case you wanted that as well.
Now in your exception handling method use the HttpServletRequest as an argument and use that to your advantage.
#ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
if (req instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) req;
log.error(new String(wrapper.getContentAsByteArray()));
}
return "greeting from #ExceptionHandler";
}
It could be that multiple filters add a wrapper to the HttpServletRequest so you might need to iterate over those wrappers, you could also use this
private Optional<ContentCachingRequestWrapper> findWrapper(ServletRequest req) {
ServletRequest reqToUse = req;
while (reqToUse instanceof ServletRequestWrapper) {
if (reqToUse instanceof ContentCachingRequestWrapper) {
return Optional.of((ContentCachingRequestWrapper) reqToUse);
}
reqToUse = ((ServletRequestWrapper) reqToUse).getRequest();
}
return Optional.empty();
}
Your exception handler would then look something like this
#ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
Optional<ContentCachingRequestWrapper) wrapper = findWrapper(req);
wrapper.ifPresent(it -> log.error(new String(it.getContentAsByteArray())));
return "greeting from #ExceptionHandler";
}
But that might depend on your filter order and if there are multiple filters adding wrappers.
Following #M.Deinum's comments, I solved and hope useful for others:
Add a Filter
public class MyFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(httpServletRequest);
chain.doFilter(cachedRequest, response);
}
}
Inject the ContentCachingRequestWrapper in ExceptionHandler
#ExceptionHandler({ HttpMessageNotReadableException.class })
public String addStudent(HttpMessageNotReadableException e, ContentCachingRequestWrapper cachedRequest) {
log.error(e.getMessage());
try {
String requestBody = IOUtils.toString(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding());
log.error(requestBody);
} catch (IOException ex) {
ex.printStackTrace();
}
return "greeting from #ExceptionHandler";
}

ContainerResponseFilter runs twice, header not modified

A filter intercepts all requests and adds a custom header to the response.
When I implement ContainerResponseFilter, the filter method runs twice:
#Provider
public class MyCustomFilter implements ContainerResponseFilter throws IOException {
#Override
public void filter(ContainerRequestContext containerRequestContext,
ContainerResponseContext containerResponseContext) {
containerResponseContext.getHeaders().add("My-Key", "myvalue");
}
}
And, the response that is served to the client does not contain the header.
I've tried debugging both the Jetty and Jersey configuration, and have narrowed the problem down to the following legacy controller logic I can't modify:
OutputStream out = response.getOutputStream();
PrintWriter printout = new PrintWriter(out);
JSONObject obj = new JSONObject(dummyData);
printout.print(obj);
It seems that writing directly to the response stream triggers some JAX-RS lifecycle chain that leads to an extra processing phase.
I'm on Jersey 2.52.1 and Jetty 9.4.4.
Solved it.
It seems that accessing outputstream and doing printwriter.print() triggered the HttpServletResponse "WRITE" status and starts returning the response to the client.
So in my controller:
#GET
#Path("/seatemperatures")
public HttpServletResponse modifyResponse(#Context HttpServletRequest request, #Context HttpServletResponse response) {
UtilClass.workOnResponse(request, response); //this uses printwriter.write()
return response;
}
I had to change the return type to void:
#GET
#Path("/seatemperatures")
public void modifyResponse(#Context HttpServletRequest request, #Context HttpServletResponse response) {
UtilClass.workOnResponse(request, response);
}
Returning the response object from the controller seems to cause an internal 500 error and effectively tried returning two response objects.

Consuming HttpServletRequest Multiple Times and Chain Between Methods

I have to read HttpServletRequest multiple times. I have wrapped HttpServletRequest like said in those posts Http Servlet request lose params from POST body after read it once
In my filter class which extends AbstractAuthenticationProcessingFilter, i can consume and chain request in successfulAuthentication method since it has chain parameter. But in addition to those solutions i have to chain request between attempt and succesful authentication steps:
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
// wrapping request and consuming
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// Since i couldn't chain wrapped httpServletRequest from attemptAuthentication step, this request still gets non-wrapping one and inputstream is empty
}
How can i pass wrapped request from attemptAuthentication to successfulAuthentication?
This is an old question but in case someone gets the same problem:
You can wrap a request like suggested in the previous answer but you need to wrap it before it gets filtered by Authentication filter:
Spring security configuration will look something like this:
#Override
protected void configure(HttpSecurity http) throws Exception {
AuthFilter authFilter = new AuthFilter();
WrapperFilter wrapperFilter = new WrapperFilter();
http .cors()
.and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(exceptionHandler)
.and()
.authorizeRequests()
.antMatchers("/v1/*", "/api/*")
.authenticated()
.and()
.addFilterBefore(authFilter, BasicAuthenticationFilter.class)
.addFilterBefore(wrapperFilter, AuthFilter.class);
}
So that wrapper filter goes before your auth filter and wrapper filter's doFilter wraps the request and passes it on:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
MultiReadHttpServletRequest wrapper = new MultiReadHttpServletRequest((HttpServletRequest) request);
chain.doFilter(wrapper, response);
}
And MultiReadHttpServletRequest is the following:
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private byte[] body;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
try {
body = IOUtils.toByteArray(request.getInputStream());
} catch (IOException ex) {
body = new byte[0];
}
}
#Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
ByteArrayInputStream wrapperStream = new ByteArrayInputStream(body);
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setReadListener(ReadListener readListener) {
}
#Override
public int read() throws IOException {
return wrapperStream.read();
}
};
}
}
There is no such way. The only thing you can do is wrap the request every time you gonna read it in one more filter. Just wrap the request every time before you read it copying body.
See some working code for a wrapper e.g. here

Spring boot HandlerInterceptor loadbalancing

I'm implementing a (sort of) load balancing HandlerInterceptor using Spring Boot.
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if (shouldUseServer1(uri)) {
response.sendRedirect(server1Uri);
} else {
response.sendRedirect(server2Uri);
}
}
The idea is, that based on the url, we either redirect to one service or another. The application doesn't have any explicit RequestMappings (yet).
Now the problem is, when the interceptor is called, the request is redirected to the default Spring error handler. As a result the URI stored in the HttpServletRequest is replaced by /error (effectively denying the access to the original URI).
Is there any way to intercept a request before it is rerouted to the error handler (or to get the original uri)?
EDIT:
Because of the way Spring MVC handles requests with no mapping, you'll either need a filter:
#Component
public class CustomFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
request.getSession().setAttribute("ORIGINAL_REQUEST_URI", request.getRequestURI());
chain.doFilter(request, response);
// alternatively, ignore the last 2 lines
// and just do your redirects from here
// and don't continue the filter chain
}
#Override
public void destroy() {}
#Override
public void init(FilterConfig arg0) throws ServletException {}
}
Otherwise, if you'd rather not rely on the session, you'll need to make the DispatcherServlet throw an exception in case no handler mapping is found, and then send the redirect from a #ControllerAdvice error handler:
#ControllerAdvice
class NoHandlerFoundExceptionExceptionHandler {
#ExceptionHandler(value = NoHandlerFoundException.class)
public ModelAndView
defaultErrorHandler(HttpServletRequest req, NoHandlerFoundException e) throws Exception {
String uri = // resolve the URI
return new ModelAndView("redirect:" + uri);
}
}
To avoid duplication, you may want to have a common class that you'll call from both the interceptor and the error handler.

Categories