When we usually use SpringMVC, as long as the request is processed by DispatcherServlet, you can customize the handling logic of different types of exceptions through @ControllerAdvice and @ExceptionHandler, you can refer to ResponseEntityExceptionHandler and DefaultHandlerExceptionResolver, the underlying principle is very simple, that is, when an exception occurs, search for the exception handler that already exists in the container and match the corresponding exception type, after a successful match, use the specified exception handler to return The result of the Response rendering, if the default exception handler can not be found then the default for the bottom (personally, I think that Spring in many functional design time have this “there is the use of custom, no use the default to provide” this idea is very elegant).

The custom exception system provided in SpringMVC doesn’t work in Spring-WebFlux for the simple reason that the underlying runtime containers are not the same. WebExceptionHandler is the top-level interface to the exception handler of Spring-WebFlux, so it can be traced back to the subclass DefaultErrorWebExceptionHandler which is the global exception handler of Spring Cloud Gateway, with the configuration class ErrorWebFluxAutoConfiguration.

Why custom exception handling

Let’s start by drawing a hypothetical but close to actual architecture diagram to locate the role of the gateway.

The role of the gateway in the overall architecture is:

  1. Routing requests from the server-side application to the back-end application.
  2. (Aggregation) Response forwarding from the back-end application to the server-side application

Assuming that the gateway service is always normal.

For point 1, assuming that the back-end application can not be smoothly and losslessly online, there is a certain chance that the gateway will route the request to some back-end “zombie node (request routing past the time when the application is better in restart or just stop)”, this time the route will fail to throw an exception, the general situation is Connection Refuse.

For point 2, assuming that the back-end application does not handle the exception correctly, then the exception information should be forwarded back to the server-side application through the gateway, which in theory will not be an exception.

In fact, there is a hidden problem in point 3, if the gateway does not only assume the function of routing, but also contains the functions of authentication, flow restriction, etc. If these functions are developed without perfect processing of exception capture or even the logic itself exists BUG, it may lead to the exception not being properly captured and processed, and the default exception handler DefaultErrorWebExceptionHandler, the processing logic of the default exception handler may not meet our expected results.

How to customize exception handling

We can start by looking at the configuration class ErrorWebFluxAutoConfiguration for the default exception handler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@AutoConfigureBefore(WebFluxAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
public class ErrorWebFluxAutoConfiguration {

	private final ServerProperties serverProperties;

	private final ApplicationContext applicationContext;

	private final ResourceProperties resourceProperties;

	private final List<ViewResolver> viewResolvers;

	private final ServerCodecConfigurer serverCodecConfigurer;

	public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties,
			ResourceProperties resourceProperties,
			ObjectProvider<ViewResolver> viewResolversProvider,
			ServerCodecConfigurer serverCodecConfigurer,
			ApplicationContext applicationContext) {
		this.serverProperties = serverProperties;
		this.applicationContext = applicationContext;
		this.resourceProperties = resourceProperties;
		this.viewResolvers = viewResolversProvider.orderedStream()
				.collect(Collectors.toList());
		this.serverCodecConfigurer = serverCodecConfigurer;
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class,
			search = SearchStrategy.CURRENT)
	@Order(-1)
	public ErrorWebExceptionHandler errorWebExceptionHandler(
			ErrorAttributes errorAttributes) {
		DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(
				errorAttributes, this.resourceProperties,
				this.serverProperties.getError(), this.applicationContext);
		exceptionHandler.setViewResolvers(this.viewResolvers);
		exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
		exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
		return exceptionHandler;
	}

	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class,
			search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes(
				this.serverProperties.getError().isIncludeException());
	}
}

Notice that the two Bean instances ErrorWebExceptionHandler and DefaultErrorAttributes both use the @ConditionalOnMissingBean annotation, which means we can override them with a custom implementation. First customize a CustomErrorWebFluxAutoConfiguration (except for the custom implementation of ErrorWebExceptionHandler, which is a direct copy of ErrorWebFluxAutoConfiguration).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@AutoConfigureBefore(WebFluxAutoConfiguration.class)
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class CustomErrorWebFluxAutoConfiguration {

    private final ServerProperties serverProperties;

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public CustomErrorWebFluxAutoConfiguration(ServerProperties serverProperties,
                                               ResourceProperties resourceProperties,
                                               ObjectProvider<ViewResolver> viewResolversProvider,
                                               ServerCodecConfigurer serverCodecConfigurer,
                                               ApplicationContext applicationContext) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers = viewResolversProvider.orderedStream()
                .collect(Collectors.toList());
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class,
            search = SearchStrategy.CURRENT)
    @Order(-1)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        // TODO Here completes the custom ErrorWebExceptionHandler implementation logic
        return null;
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
    }
}

The implementation of ErrorWebExceptionHandler can directly refer to DefaultErrorWebExceptionHandler, or even directly inherit from DefaultErrorWebExceptionHandler and override the corresponding method. Here the exception information is directly encapsulated into the following format Response return, and finally need to be rendered into JSON format.

1
2
3
4
5
6
{
  "code": 200,
  "message": "Description Information",
  "path" : "Request Path",
  "method": "Request Method"
}

We need to analyze some source code in DefaultErrorWebExceptionHandler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Encapsulating Exception Properties
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
	return this.errorAttributes.getErrorAttributes(request, includeStackTrace);
}

// Render ExceptionResponse
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
	boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
	Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
	return ServerResponse.status(getHttpStatus(error))
			.contentType(MediaType.APPLICATION_JSON_UTF8)
			.body(BodyInserters.fromObject(error));
}

// Returns a ServerResponse-based object for the routing method
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
	return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);
}

// The encapsulation of the HTTP response status code was originally parsed based on the status attribute of the exception property
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
	int statusCode = (int) errorAttributes.get("status");
	return HttpStatus.valueOf(statusCode);
}

Identify three points.

  1. the final object wrapped into the response body is derived from DefaultErrorWebExceptionHandler#getErrorAttributes() and the result is a sequence of bytes converted to a Map<String, Object> instance.
  2. The original RouterFunction implementation only supports HTML format return, we need to modify it to JSON format return (or support all formats return).
  3. DefaultErrorWebExceptionHandler#getHttpStatus() is a wrapper for the response status code, the original logic is based on the status attribute of the exception property getErrorAttributes() for parsing.

The custom JsonErrorWebExceptionHandler is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

    public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes,
                                        ResourceProperties resourceProperties,
                                        ErrorProperties errorProperties,
                                        ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
    }

    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        // Here the logic can actually be customized according to the exception type
        Throwable error = super.getError(request);
        Map<String, Object> errorAttributes = new HashMap<>(8);
        errorAttributes.put("message", error.getMessage());
        errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        errorAttributes.put("method", request.methodName());
        errorAttributes.put("path", request.path());
        return errorAttributes;
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    @Override
    protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
        // Here you can actually customize the HTTP response code based on the attributes inside the errorAttributes
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

配置类CustomErrorWebFluxAutoConfiguration添加JsonErrorWebExceptionHandler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Bean
@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT)
@Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
    JsonErrorWebExceptionHandler exceptionHandler = new JsonErrorWebExceptionHandler(
                errorAttributes,
                resourceProperties,
                this.serverProperties.getError(),
                applicationContext);
    exceptionHandler.setViewResolvers(this.viewResolvers);
    exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
    exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
    return exceptionHandler;
}

Very simple, here the HTTP response status code of the exception is unified as HttpStatus.INTERNAL_SERVER_ERROR(500), the transformation is not much, as long as you understand the contextual logic of the original exception handling.

Testing

Test scenario 1: Direct invocation of a downstream service without starting only the gateway and the downstream service.

1
2
3
4
curl http://localhost:9090/order/host

// response
{"path":"/order/host","code":500,"message":"Connection refused: no further information: localhost/127.0.0.1:9091","method":"GET"}

Test scenario 2: The downstream service is started and invoked normally, and the gateway itself throws an exception.

Customize a global filter in the gateway application and intentionally throw an exception.

1
2
3
4
5
6
7
8
9
@Component
public class ErrorGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        int i = 1/0;
        return chain.filter(exchange);
    }
}
1
2
3
4
curl http://localhost:9090/order/host

// response
{"path":"/order/host","code":500,"message":"/ by zero","method":"GET"}

The response results are consistent with the custom logic, and the backend logs print the corresponding exception stacks.

Summary

The author has always believed that doing exception classification and processing according to the classification is a very important part of the project. In the system I am responsible for in my company, I insist on implementing exception classification and capture, mainly because I need to distinguish between exceptions that can be retried to compensate and those that cannot be retried and need timely warning, so that I can customize the self-healing logic for recoverable exceptions and timely warning and human intervention for those that cannot be recovered. Therefore, Spring Cloud Gateway, the technology stack, must also investigate its custom exception handling logic.


Reference https://www.throwx.cn/2019/05/11/spring-cloud-gateway-custom-exception-handler/