Skip to content

Commit

Permalink
Merge pull request #91 from wimdeblauwe/feature/gh-87_handle-filter-c…
Browse files Browse the repository at this point in the history
…hain-exceptions

Handle filter chain exceptions
  • Loading branch information
wimdeblauwe authored Apr 30, 2024
2 parents 7331216 + 1ce5817 commit 5710b49
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 85 deletions.
11 changes: 11 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,13 @@ public class FallbackExceptionHandler extends ErrorHandlingControllerAdvice {
}
----

=== Handle filter exceptions

By default, the library will not handle exceptions from custom filters.
Those are implementations of `jakarta.servlet.Filter`, usually subclasses of `org.springframework.web.filter.OncePerRequestFilter` in a Spring Boot application.

By setting the property `error.handling.handle-filter-chain-exceptions` to `true`, the library will handle those exceptions and return error responses just like is done for exceptions coming from controller methods.

== Custom exception handler

If the <<Configuration,extensive customization options>> are not enough, you can write your own `ApiExceptionHandler` implementation.
Expand Down Expand Up @@ -1466,6 +1473,10 @@ When this is set to `true`, you can use any superclass from your `Exception` typ
|error.handling.add-path-to-error
|This property allows to remove the `path` property in the error response when set to `false`.
|`true`

|error.handling.handle-filter-chain-exceptions
|Set this to `true` to have the library intercept any exception thrown from custom filters and also have the same error responses as exceptions thrown from controller methods.
|`false`.
|===

== Support
Expand Down
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);
}
}
}
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 5710b49

Please sign in to comment.