I have a filter for logging some information for each request to a Spring Boot application. Some of this information I need to extract from the body. That's not a problem in itself, but to do so I use ContentCachingResponseWrapper, and that is messing up my unit tests.
Here is a simplified version of my filter:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
var wrappedResponse = response instanceof ContentCachingResponseWrapper ? (ContentCachingResponseWrapper) response : new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, wrappedResponse);
} finally {
System.out.println("Response body: " + new String(wrappedResponse.getContentAsByteArray()));
wrappedResponse.copyBodyToResponse();
}
}
And here is a simplified version of my test:
void myTest() throws ServletException, IOException {
final String body = "This is a body that my service might return.";
var testResp = new MockHttpServletResponse();
testResp.getWriter().print(body);
testResp.getWriter().flush();
testResp.setContentLength(body.length());
myFilter.doFilterInternal(Mockito.mock(HttpServletRequest.class), testResp, Mockito.mock(FilterChain.class));
}
The problem is that when running my tests, wrappedResponse.getContentAsByteArray() returns an empty array.
There are 2 things wrong with your code
Your filter isn't wrapping the response in the ContentCachingResponseWrapper
You are writing the response before the wrapping has occured on the underlying response, so the ContentCachingResponseWrapper has no change of caching the response.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
var wrappedResponse = response instanceof ContentCachingResponseWrapper ? (ContentCachingResponseWrapper) response : new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, wrappedResponse);
} finally {
System.out.println("Response body: " + new String(wrappedResponse.getContentAsByteArray()));
wrappedResponse.copyBodyToResponse();
}
}
Now the response will be wrapped in the wrapper for responses written further down the FilterChain. This is also something you can leverage in your testcase by mocking the FilterChain and write the response in an answer.
void myTest() throws ServletException, IOException {
var body = "This is a body that my service might return.";
var req = new MockHttpServletRequest();
var res = new MockHttpServletResponse();
var mockChain = Mockito.mock(FilterChain.class);
Mockito.when(mockChain.doFilter(any(), any())
.thenAnswer((it -> {
var response = it.getArgument(1, HttpServletResponse.class);
response.getWriter().print(body);
response.getWriter().flush();
response.setContentLength(body.length());
return null;
});
myFilter.doFilterInternal(req, res, mockChain);
}
Something along these lines should do the trick.
Related
I want to save request and response in String format to database when endpoint /v2 is called.
This implementation workes fine and save is correct but if i call to v1 endpoint, is muted, response is null, just 200 OK in Postman. Normally it response with json {id="123456789"} Whats wrong have i in this implementation that endpoint v1 doesnt work ? If i delete CachingRequestBodyFilter class, v1 works fine but when v2 is called, nothing saved to database.
v1 endpoint is for xml and v2 endpoint is for json format.
Meybe is better way to save request and response to db via interceptors?
#AllArgsConstructor
#Component
#Data
public class RequestInterceptor implements HandlerInterceptor {
private final RequestService requestService;
#Override
public void afterCompletion(#NonNull HttpServletRequest request, #NonNull HttpServletResponse response, #NonNull Object handler, Exception ex){
requestService.saveResponse(request, response, ex);
}
}
public void saveResponse(HttpServletRequest request, HttpServletResponse response, Exception ex) {
try {
String requestBody = getRequestAsString(request);
String responseBody = getResponseAsString(response);
buildMessages(requestBody, responseBody, ex);
} catch (IOException e) {
e.printStackTrace();
}
}
private String getRequestAsString(HttpServletRequest request) throws IOException {
ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
return new String(requestWrapper.getContentAsByteArray(), requestWrapper.getCharacterEncoding());
}
private String getResponseAsString(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response;
byte[] responseArray = responseWrapper.getContentAsByteArray();
String characterEncoding = responseWrapper.getCharacterEncoding();
responseWrapper.copyBodyToResponse();
return new String(responseArray, characterEncoding);
}
method buildMessages() is just a builder object which i want to save to db.
filter class:
#Component
#WebFilter(filterName = "CachingRequestBodyFilter", urlPatterns = {"/v2/*"})
public class CachingRequestBodyFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
HttpServletResponse response = (HttpServletResponse) res;
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
}
i want to save my request and response to db when endpoint v2 is called and when v1 is called response is not null;
Hi I have some existing code in my application that use a security filter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (someCondition==TRUE) {
super.doFilter(request, response, chain);
}else{
chain.doFilter(request, response);
}
}
Now I want to redirect the user to a certain URL after the filter flow is completed. I have added the sendRedirect Method like the below example in code, but this is breaking the filter flow in some cases.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (someCondition==TRUE) {
String targetURL = request.getParameter("targetURL");
if(targetURL != null && targetURL != "undefined") {
LOGGER.info("redirecting to this targetURL : " + targetURL);
response.sendRedirect(targetURL);
// this is an internal URL and should be redirected after executing the filter flow
} else {
super.doFilter(request, response, chain);
}
}else{
chain.doFilter(request, response);
}
}
I have a feeling that this is not correct way to redirect the User to an internal URL as it breaks the filter flow. What is correct way of redirecting to URL after filter processing has been completed?
I have a custom filter that is used to authenticate the user. I am always getting full authentication requried error even though I have thrown a custom exception with specific message & added exception handler as well.
Code for filter:
#Slf4j
#Component
public classTokenValidationFilter extends OncePerRequestFilter {
#Autowired
private TokenValidationHelper tokenValidationHelper;
#Override
protected void doFilterInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse)servletResponse;
MultiReadRequestWrapper request = new MultiReadRequestWrapper(httpRequest);
SecurityContext context = SecurityContextHolder.getContext();
// check if already authenticated
if (context.getAuthentication() == null) {
Authentication authentication =
tokenValidationHelper.validateAndAuthenticate(request);
context.setAuthentication(authentication);
}
filterChain.doFilter(request, httpResponse);
}
}
Code for exception handler:
#ControllerAdvice
public class ExceptionHandler {
#ExceptionHandler({IrrecoverableAuthException.class})
#ResponseBody
#ResponseStatus(HttpStatus.UNAUTHORIZED)
public RegistrationErrorResponse handleInternalServerException(IrrecoverableAuthException exception) {
return getErrorResponse(exception , Category.Error exception.getMessage());
}
}
But still getting wrong message
"Full authentication access is required to access this resource"
Exception handler won't be invoked from within the filter. You can use HttpServletResponse from within the filter and write your error response manually as follows:
protected void onFailedAuthentication(
HttpServletRequest request,
HttpServletResponse response,
IrrecoverableAuthException failed) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(failed.getStatus().getStatusCode());
try (OutputStream out = response.getOutputStream()) {
out.write(MAPPER.writeValueAsBytes(getErrorResponse())); // build the required response here
out.flush();
} catch (IOException e) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
Call this method from your filter
#Slf4j
#Component
public classTokenValidationFilter extends OncePerRequestFilter {
#Autowired
private TokenValidationHelper tokenValidationHelper;
#Override
protected void doFilterInternal(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest httpRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse)servletResponse;
MultiReadRequestWrapper request = new MultiReadRequestWrapper(httpRequest);
SecurityContext context = SecurityContextHolder.getContext();
// check if already authenticated
if (context.getAuthentication() == null) {
try {
Authentication authentication =
tokenValidationHelper.validateAndAuthenticate(request);
context.setAuthentication(authentication);
} catch(IrrecoverableAuthException ex) {
onFailedAuthentication(httpRequest, httpResponse, ex);
}
}
filterChain.doFilter(request, httpResponse);
}
}
I'm trying to write a servlet filter that will add headers to the response depending on the status of the request. I know I have to wrap the response with a HttpServletResponseWrapper before passing to the chain.doFilter but the headers never get sent, so I'm obviously missing something very obvious.
Code looks something like:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(httpServletResponse);
chain.doFilter(request, responseWrapper);
if(responseWrapper.getStatus() < 400)
{
responseWrapper.addHeader("X-Custom-Foobar", "abc");
}
}
Is there something I have to capture in the wrapper to prevent the response from going out to the client until the check is complete?
So the frustrating part about this spec is that you have to completely intercept the ServletOutputStream and buffer it. I ended up following the example here :: https://stackoverflow.com/a/11027170/76343
The base class HttpServletResponseWrapper is a complete passthrough and as soon as the output stream is closed, all further modifications to the response are mute.
Unfortunately there doesn't seem to be a more elegant way to accomplish this.
You need to extend HttpResponseWrapper() and override the appropriate methods. Just using a vanilla HttpResponseWrapper by itself accomplishes exactly nothing.
The order of this code is inverted:
chain.doFilter(request, responseWrapper);
if(responseWrapper.getStatus() < 400)
{
responseWrapper.addHeader("X-Custom-Foobar", "abc");
}
Try this instead:
if(responseWrapper.getStatus() < 400)
{
responseWrapper.addHeader("X-Custom-Foobar", "abc");
}
chain.doFilter(request, responseWrapper);
The doFilter method does not return to your method until after the response has been sent on the wire.
This is actually possible. But because after calling chain.doFilter(request, response) the response is already committed, we have to set the headers after receiving the status code, but before the response is committed. Here is an example:
public class HeadersFilter implements Filter {
#Override
public void init(FilterConfig filterConfig) {
}
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
chain.doFilter(request, new ResponseWrapper(response));
}
public static class ResponseWrapper extends HttpServletResponseWrapper {
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
#Override
public void setStatus(int sc) {
super.setStatus(sc);
// SET YOUR HEADERS HERE
// setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
}
}
#Override
public void destroy() {
}
}
With the above code I always get an error in line of test
when(request.getServletContext().getAttribute("SessionFactory"))
.thenReturn(factory);
Any ideas?
Java class
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
SessionFactory sessionFactory = (SessionFactory) request.getServletContext().getAttribute("SessionFactory");
...............
}
Test class
#Test
public void testServlet() throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
factory = contextInitialized();
when(request.getServletContext().getAttribute("SessionFactory")).thenReturn(factory); //Always error here
when(request.getParameter("empId")).thenReturn("35");
PrintWriter writer = new PrintWriter("somefile.txt");
when(response.getWriter()).thenReturn(writer);
new DeleteEmployee().doGet(request, response);
verify(request, atLeast(1)).getParameter("username"); // only if you want to verify username was called...
writer.flush(); // it may not have been flushed yet...
assertTrue(FileUtils.readFileToString(new File("somefile.txt"), "UTF-8")
.contains("My Expected String"));
}
when(request.getServletContext().getAttribute("SessionFactory")).thenReturn(factory);
This bit:
request.getServletContext().getAttribute("SessionFactory")
is a chained call; you're trying to stub both the request, and the servlet context that the request returns.
You can do that, but you need to use deep stubs:
HttpServletRequest request = mock(HttpServletRequest.class, RETURNS_DEEP_STUBS);