Search code examples
springservlet-filtersspring-boot-3

How to set custom response in Servlet Filter on Spring Boot 3? (Response seems to be replaced later)


I have this Filter class in Spring Boot 3:

@Component
public class AcknowledgeFilter implements jakarta.servlet.Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // does not work
        ((HttpServletResponse) response).sendError(500, "my-error-message");

        // does not work
        // response.getWriter().write("my-error-message");
        // response.getWriter().flush();
        // or response.getWriter().close();

        // does not work either
        response.getOutputStream().write("my-error-message".getBytes());
        return;
    }
}

The method returns without calling chain.doFilter() but just with setting the error response, the proper HTTP code is returned (500 or 404, ...) but never the response message I set (here "my-error-message"). I always get an JSON response like:

{
  "timestamp": "2023-10-17T15:05:12.825+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/api/myrequest/path"
}

Where does this message come from and how can I overwrite it?

Update

I turned on debug logging

o.s.security.web.FilterChainProxy        : Secured GET /api/order/last?customerNumber=123

o.a.c.c.C.[Tomcat].[localhost]           : Processing ErrorPage[errorCode=0, location=/api/error]
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
o.s.security.web.FilterChainProxy        : Securing GET /api/error?customerNumber=123
o.s.security.web.FilterChainProxy        : Secured GET /api/error?customerNumber=123
o.s.web.servlet.DispatcherServlet        : "ERROR" dispatch for GET "/api/error?customerNumber=123", parameters={masked}
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
c.a.c.c.c.EndpointLoggingInterceptor     : Endpoint /api/error called
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)

This shows, that after the error is returned, it is processed by the BasicErrorController. It seems to be necessary, to register a custom ErrorController.


Solution

  • My solution was to overwrite Spring Boots BaseErrorController:

    import jakarta.servlet.RequestDispatcher;
    import jakarta.servlet.http.HttpServletRequest;
    import org.springframework.boot.autoconfigure.web.ErrorProperties;
    import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
    import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.util.ObjectUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class ErrorController extends BasicErrorController {
        public ErrorController() {
            super(new DefaultErrorAttributes(), new ErrorProperties());
        }
    
        @Override
        @RequestMapping
        public ResponseEntity error(HttpServletRequest request) {
            final String message = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
            if (!ObjectUtils.isEmpty(message)) {
                final HttpStatus status = getStatus(request);
                final HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return new ResponseEntity<>(message, headers, status);
            }
            return super.error(request);
        }
    }
    
    

    This is certainly not perfect, but was a quick solution that works.