Error Handling in (Spring) Servlet Filters

If you're using Java Servlet Filters, you've likely come to the situation where you need to fail the request, for instance if there's a mandatory parameter missing, or the request is otherwise deemed to be invalid.
Let's say that for instance you're using a filter for tracking the correlation-id header:
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String correlationId = request.getHeader("correlation-id");
if (null == correlationId || !Patterns.UUID_V4.matcher(correlationId).matches()) {
// only allow UUIDs, if it's not valid according to our contract, allow it to be rewritten
// alternatively, we would reject the request with an HTTP 400 Bad Request, as a client
// hasn't fulfilled the contract
correlationId = UUID.randomUUID().toString();
}
// make sure that the Mapped Diagnostic Context (MDC) has the `correlationId` so it can then
// be populated in the logs
try (MDC.MDCCloseable closable = MDC.putCloseable("correlationId", correlationId)) {
response.addHeader("correlation-id", correlationId); // so the response contains it, too
filterChain.doFilter(request, response);
}
}
}
You may want to reach for something like this:
if (null == correlationId || !Patterns.UUID_V4.matcher(correlationId).matches()) {
throw new CorrelationIdMalformedException();
}
Which can then have a corresponding exception handler:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CorrelationIdMalformedException.class)
protected ResponseEntity<ErrorResponse> handleCorrelationIdMalformedException(
CorrelationIdMalformedException ex, WebRequest request) {
// i.e.
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setError(ErrorResponse.Error.INVALID_REQUEST);
errorResponse.setErrorDescription(ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
}
Unfortunately this is a common pitfall, and as noted in this conversation on StackOverflow is not possible because the Servlet Filter executes before Spring's own DispatchServlet, so we don't have the ability to do this.
Instead, we need to use the Servlet API to return the error, for instance using our handy new method handleInvalidCorrelationId:
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private final ObjectMapper objectMapper;
public CorrelationIdFilter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uuid = request.getHeader("correlation-id");
if (uuid == null) {
uuid = UUID.randomUUID().toString();
}
if (!uuid.matches(Patterns.UUID_STRING)) {
handleInvalidCorrelationId(response);
return; // make sure you have this set!
}
try {
filterChain.doFilter(request, response);
} finally {
response.addHeader("correlation-id", uuid);
}
}
private void handleInvalidCorrelationId(HttpServletResponse response) throws IOException {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setError(ErrorResponse.Error.INVALID_REQUEST);
errorResponse.setErrorDescription("The correlation-id is not a valid UUID.");
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
This will reject the request correctly, and allow us to specify the response's headers and body correctly.