Skip to content

Commit

Permalink
Handle exceptions from filter chain
Browse files Browse the repository at this point in the history
This commit adds a FilterChainExceptionHandlerFilter that allows to handle an exception thrown from a Filter in the same way as we do for exceptions thrown from controllers.

It is not enabled by default for backwards compatibility. Use `error.handling.handle-filter-chain-exceptions=true` to enable it.

Fixes #87
  • Loading branch information
wimdeblauwe committed Apr 24, 2024
1 parent 0d373d1 commit f717389
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,30 @@
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;

import java.util.List;

public abstract class AbstractErrorHandlingConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractErrorHandlingConfiguration.class);

@Bean
@ConditionalOnMissingBean
public ErrorHandlingFacade errorHandlingFacade(List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackHandler,
LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
handlers.sort(AnnotationAwareOrderComparator.INSTANCE);
LOGGER.info("Error Handling Spring Boot Starter active with {} handlers", handlers.size());
LOGGER.debug("Handlers: {}", handlers);

return new ErrorHandlingFacade(handlers, fallbackHandler, loggingService, responseCustomizers);
}

@Bean
@ConditionalOnMissingBean
public LoggingService loggingService(ErrorHandlingProperties properties) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ErrorHandlingFacade {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorHandlingFacade.class);

private final List<ApiExceptionHandler> handlers;
private final FallbackApiExceptionHandler fallbackHandler;
private final LoggingService loggingService;
private final List<ApiErrorResponseCustomizer> responseCustomizers;

public ErrorHandlingFacade(List<ApiExceptionHandler> handlers, FallbackApiExceptionHandler fallbackHandler, LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
this.handlers = handlers;
this.fallbackHandler = fallbackHandler;
this.loggingService = loggingService;
this.responseCustomizers = responseCustomizers;
}

public ApiErrorResponse handle(Throwable exception) {
ApiErrorResponse errorResponse = null;
for (ApiExceptionHandler handler : handlers) {
if (handler.canHandle(exception)) {
errorResponse = handler.handle(exception);
break;
}
}

if (errorResponse == null) {
errorResponse = fallbackHandler.handle(exception);
}

for (ApiErrorResponseCustomizer responseCustomizer : responseCustomizers) {
responseCustomizer.customize(errorResponse);
}

loggingService.logException(errorResponse, exception);

return errorResponse;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class ErrorHandlingProperties {

private boolean searchSuperClassHierarchy = false;

private boolean handleFilterChainExceptions = false;

public boolean isEnabled() {
return enabled;
}
Expand Down Expand Up @@ -143,6 +145,14 @@ public void setAddPathToError(boolean addPathToError) {
this.addPathToError = addPathToError;
}

public boolean isHandleFilterChainExceptions() {
return handleFilterChainExceptions;
}

public void setHandleFilterChainExceptions(boolean handleFilterChainExceptions) {
this.handleFilterChainExceptions = handleFilterChainExceptions;
}

public enum ExceptionLogging {
NO_LOGGING,
MESSAGE_ONLY,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter.reactive;

import io.github.wimdeblauwe.errorhandlingspringbootstarter.*;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponse;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponseCustomizer;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
Expand All @@ -13,30 +16,20 @@
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Locale;

public class GlobalErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalErrorWebExceptionHandler.class);

private final List<ApiExceptionHandler> handlers;
private final FallbackApiExceptionHandler fallbackHandler;
private final LoggingService loggingService;
private final List<ApiErrorResponseCustomizer> responseCustomizers;
private final ErrorHandlingFacade errorHandlingFacade;

public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes,
WebProperties.Resources resources,
ErrorProperties errorProperties,
ApplicationContext applicationContext,
List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackHandler,
LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
ErrorHandlingFacade errorHandlingFacade) {
super(errorAttributes, resources, errorProperties, applicationContext);
this.handlers = handlers;
this.fallbackHandler = fallbackHandler;
this.loggingService = loggingService;
this.responseCustomizers = responseCustomizers;
this.errorHandlingFacade = errorHandlingFacade;
}

@Override
Expand All @@ -55,23 +48,7 @@ public Mono<ServerResponse> handleException(ServerRequest request) {
LOGGER.debug("webRequest: {}", request);
LOGGER.debug("locale: {}", locale);

ApiErrorResponse errorResponse = null;
for (ApiExceptionHandler handler : handlers) {
if (handler.canHandle(exception)) {
errorResponse = handler.handle(exception);
break;
}
}

if (errorResponse == null) {
errorResponse = fallbackHandler.handle(exception);
}

for (ApiErrorResponseCustomizer responseCustomizer : responseCustomizers) {
responseCustomizer.customize(errorResponse);
}

loggingService.logException(errorResponse, exception);
ApiErrorResponse errorResponse = errorHandlingFacade.handle(exception);

return ServerResponse.status(errorResponse.getHttpStatus())
.contentType(MediaType.APPLICATION_JSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,13 @@ public GlobalErrorWebExceptionHandler globalErrorWebExceptionHandler(ErrorAttrib
ObjectProvider<ViewResolver> viewResolvers,
ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext,
LoggingService loggingService,
List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackApiExceptionHandler,
List<ApiErrorResponseCustomizer> responseCustomizers) {
ErrorHandlingFacade errorHandlingFacade) {

GlobalErrorWebExceptionHandler exceptionHandler = new GlobalErrorWebExceptionHandler(errorAttributes,
webProperties.getResources(),
serverProperties.getError(),
applicationContext,
handlers,
fallbackApiExceptionHandler,
loggingService,
responseCustomizers);
errorHandlingFacade);
exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,35 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter.servlet;

import io.github.wimdeblauwe.errorhandlingspringbootstarter.*;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponse;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;

import java.util.List;
import java.util.Locale;

@ControllerAdvice(annotations = RestController.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class ErrorHandlingControllerAdvice {
private static final Logger LOGGER = LoggerFactory.getLogger(ErrorHandlingControllerAdvice.class);

private final List<ApiExceptionHandler> handlers;
private final FallbackApiExceptionHandler fallbackHandler;
private final LoggingService loggingService;
private final List<ApiErrorResponseCustomizer> responseCustomizers;
private final ErrorHandlingFacade errorHandlingFacade;

public ErrorHandlingControllerAdvice(List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackHandler,
LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
this.handlers = handlers;
this.fallbackHandler = fallbackHandler;
this.loggingService = loggingService;
this.responseCustomizers = responseCustomizers;
this.handlers.sort(AnnotationAwareOrderComparator.INSTANCE);

LOGGER.info("Error Handling Spring Boot Starter active with {} handlers", this.handlers.size());
LOGGER.debug("Handlers: {}", this.handlers);
public ErrorHandlingControllerAdvice(ErrorHandlingFacade errorHandlingFacade) {
this.errorHandlingFacade = errorHandlingFacade;
}

@ExceptionHandler
public ResponseEntity<?> handleException(Throwable exception, WebRequest webRequest, Locale locale) {
LOGGER.debug("webRequest: {}", webRequest);
LOGGER.debug("locale: {}", locale);

ApiErrorResponse errorResponse = null;
for (ApiExceptionHandler handler : handlers) {
if (handler.canHandle(exception)) {
errorResponse = handler.handle(exception);
break;
}
}

if (errorResponse == null) {
errorResponse = fallbackHandler.handle(exception);
}

for (ApiErrorResponseCustomizer responseCustomizer : responseCustomizers) {
responseCustomizer.customize(errorResponse);
}

loggingService.logException(errorResponse, exception);
ApiErrorResponse errorResponse = errorHandlingFacade.handle(exception);

return ResponseEntity.status(errorResponse.getHttpStatus())
.body(errorResponse);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter.servlet;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponse;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingFacade;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class FilterChainExceptionHandlerFilter extends OncePerRequestFilter {

private final ErrorHandlingFacade errorHandlingFacade;
private final ObjectMapper objectMapper;

public FilterChainExceptionHandlerFilter(ErrorHandlingFacade errorHandlingFacade, ObjectMapper objectMapper) {
this.errorHandlingFacade = errorHandlingFacade;
this.objectMapper = objectMapper;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

try {
filterChain.doFilter(request, response);
} catch (Exception ex) {
ApiErrorResponse errorResponse = errorHandlingFacade.handle(ex);
response.setStatus(errorResponse.getHttpStatus().value());
var jsonResponseBody = objectMapper.writeValueAsString(errorResponse);
response.getWriter().write(jsonResponseBody);

This comment has been minimized.

Copy link
@donalmurtagh

donalmurtagh Apr 26, 2024

Unless I'm missing something, I think you should only call errorHandlingFacade.handle(ex) if the isHandleFilterChainExceptions() config property is true. As far as I can tell, you're not using this new property anywhere. I was expecting this code to look like

try {
    filterChain.doFilter(request, response);
} catch (Exception ex) {
    if (errorHandlingProperties.isHandleFilterChainExceptions() {
        ApiErrorResponse errorResponse = errorHandlingFacade.handle(ex);
        response.setStatus(errorResponse.getHttpStatus().value());
        var jsonResponseBody = objectMapper.writeValueAsString(errorResponse);
        response.getWriter().write(jsonResponseBody);
    } else {
        throw ex;
    }
}

This comment has been minimized.

Copy link
@wimdeblauwe

wimdeblauwe Apr 27, 2024

Author Owner

It is used via @ConditionalOnProperty in ServletErrorHandlingConfiguration. So this filter is only loaded when the property is enabled.

This comment has been minimized.

Copy link
@donalmurtagh

donalmurtagh Apr 29, 2024

I missed that, thanks for the explanation

}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter.servlet;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.*;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.MissingRequestValueExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
Expand All @@ -10,11 +11,11 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;

import java.util.List;
import org.springframework.core.Ordered;

@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
Expand All @@ -38,13 +39,24 @@ public MissingRequestValueExceptionHandler missingRequestValueExceptionHandler(H

@Bean
@ConditionalOnMissingBean
public ErrorHandlingControllerAdvice errorHandlingControllerAdvice(List<ApiExceptionHandler> handlers,
FallbackApiExceptionHandler fallbackApiExceptionHandler,
LoggingService loggingService,
List<ApiErrorResponseCustomizer> responseCustomizers) {
return new ErrorHandlingControllerAdvice(handlers,
fallbackApiExceptionHandler,
loggingService,
responseCustomizers);
public ErrorHandlingControllerAdvice errorHandlingControllerAdvice(ErrorHandlingFacade errorHandlingFacade) {
return new ErrorHandlingControllerAdvice(errorHandlingFacade);
}

@Bean
@ConditionalOnProperty("error.handling.handle-filter-chain-exceptions")
public FilterChainExceptionHandlerFilter filterChainExceptionHandlerFilter(ErrorHandlingFacade errorHandlingFacade, ObjectMapper objectMapper) {
return new FilterChainExceptionHandlerFilter(errorHandlingFacade, objectMapper);
}

@Bean
@ConditionalOnProperty("error.handling.handle-filter-chain-exceptions")
public FilterRegistrationBean<FilterChainExceptionHandlerFilter> filterChainExceptionHandlerFilterFilterRegistrationBean(FilterChainExceptionHandlerFilter filterChainExceptionHandlerFilter) {
FilterRegistrationBean<FilterChainExceptionHandlerFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(filterChainExceptionHandlerFilter);
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);

return registrationBean;
}
}
Loading

0 comments on commit f717389

Please sign in to comment.