Spring Cloud Gateway global exception handling

Posted May 28, 20203 min read

Why you need global exception handling

In traditional Spring Boot applications, we use @ControllerAdvice to handle global exceptions and perform unified packaging and return

//Excerpt from spring cloud alibaba console module processing
@ControllerAdvice
public class ConsoleExceptionHandler {

    @ExceptionHandler(AccessException.class)
    private ResponseEntity <String> handleAccessException(AccessException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN) .body(e.getErrMsg());
    }
}

For example: An application calls a database exception and wraps the exception request response to the client through @ControllerAdvice

However, under the microservice architecture, for example, the gateway fails to call the business microservice(forwarding failure, call exception, forwarding failure), @ControllerAdvice set in the application will be invalid, because the traffic is not forwarded to the application for processing.

As shown above:Simulate that all routing assertions do not match 404, and spring boot keeps the same error output page by default. Obviously we can't solve the problem by configuring @ControllerAdvice on the gateway as well, because spring cloud gateway is based on webflux reactive programming.

Solution

Default processing flow

  • ExceptionHandlingWebHandler as part of the core WebHandler of spring cloud gateway will filter exception handling

    public class ExceptionHandlingWebHandler extends WebHandlerDecorator {

      @Override
      public Mono <Void> handle(ServerWebExchange exchange) {
          Mono <Void> completion;
          try {
              completion = super.handle(exchange);
          }
          catch(Throwable ex) {
              completion = Mono.error(ex);
          }
    
       //Get global WebExceptionHandler execution
          for(WebExceptionHandler handler:this.exceptionHandlers) {
              completion = completion.onErrorResume(ex-> handler.handle(exchange, ex));
          }
          return completion;
      }

    }

  • The default implementation DefaultErrorWebExceptionHandler

public class DefaultErrorWebExceptionHandler {

    @Override
    protected RouterFunction <ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
     //Decide what resource to return based on the client's `accpet` request header, as above the browser returns a page
        return route(acceptsTextHtml(), this ::renderErrorView) .andRoute(all(), this ::renderErrorResponse);
    }
}


//Simulate the specified `accpet` situation
curl --location --request GET 'http://localhost:9999/adminx/xx' \ 18:09:23
     --header 'Accept:application/json'
{"timestamp":"2020-05-24 18:09:24", "path":"/adminx/xx", "status":404, "error":"Not Found", "message":null, "requestId":"083c48e3-2"}  

Rewrite ErrorWebExceptionHandler

/**
* @author lengleng
* @date 2020/5/23
*


* Gateway exception general processor, only works in webflux environment, priority is lower than {@link ResponseStatusExceptionHandler}
* /
@ Slf4j
@Order(-1)
@RequiredArgsConstructor
public class GlobalExceptionConfiguration implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;

    @Override
    public Mono <Void> handle(ServerWebExchange exchange, Throwable ex) {
        ServerHttpResponse response = exchange.getResponse();

        if(response.isCommitted()) {
            return Mono.error(ex);
        }

        //header set
        response.getHeaders(). setContentType(MediaType.APPLICATION_JSON);
        if(ex instanceof ResponseStatusException) {
            response.setStatusCode(((ResponseStatusException) ex) .getStatus());
        }

        return response
                .writeWith(Mono.fromSupplier(()-> {
                    DataBufferFactory bufferFactory = response.bufferFactory();
                    try {
                        return bufferFactory.wrap(objectMapper.writeValueAsBytes(R.failed(ex.getMessage())));
                    } catch(JsonProcessingException e) {
                        log.warn("Error writing response", ex);
                        return bufferFactory.wrap(new byte [0]);
                    }
                }));
    }
}

to sum up

  • The priority of the rewritten DefaultErrorWebExceptionHandler must be less than the response code of the built-in ResponseStatusExceptionHandler to obtain the corresponding error class after it is processed
  • Other extensions can refer to SentinelBlockExceptionHandler sentinel integrated gateway processing, but there is no difference between the overall and default exception handling
  • Basic environment description:Spring Cloud Hoxton.SR4 & Spring Boot 2.3.0
  • Specific implementation code reference: https://gitee.com/log4j/pig

image