Spring Cloud Gateway filter accurately controls exception return (analysis)

Welcome to my GitHub

Here we classify and summarize all the original works of Xinchen (including supporting source code): https://github.com/zq2599/blog_demos

Overview of this article

  • stay Spring Cloud Gateway modifying the content of the request and response body In the article, we successfully modified the content of the request body through the filter, leaving a problem: if an exception occurs in the filter (for example, the request parameters are illegal), when the exception information is thrown, the return code and body received by the caller are processed by the Spring Cloud Gateway framework, and the caller cannot know the real cause of the error according to these contents, as shown in the following figure:

  • The task of this article is to analyze the causes of the above phenomena and find out the specific logic of the return code and response body generation by reading the source code

Advance summary

  • Here, the analysis results are summarized in advance. If you are busy and don't have much time but want to know the final reason, you can directly focus on the following summary:
  1. In the Spring Cloud Gateway application, there is a bean of ErrorAttributes type, and its getErrorAttributes method returns a map
  2. When the application throws an exception, the return code comes from the status value of the above map, and the returned body is the result of the whole map serialization
  3. By default, the implementation class of ErrorAttributes is DefaultErrorAttributes
  • Look at the status value of the above map (that is, the return code of the response) and how the DefaultErrorAttributes are generated:
  1. First, check whether the exception object is of ResponseStatusException type
  2. If it is the ResponseStatusException type, the getStatus method of the exception object is called as the return value
  3. If it is not a ResponseStatusException type, see if the exception class has a ResponseStatus annotation,
  4. If so, take the code attribute of the annotation as the return value
  5. If the exception object is neither ResponseStatusException type nor ResponseStatus annotation, 500 is returned
  • Finally, look at the message field of the map (that is, the message field of the response body) and how the DefaultErrorAttributes are generated:
  1. Is the exception object of type BindingResult
  2. If it is not BindingResult type, it depends on whether it is ResponseStatusException type
  3. If so, use getReason as the return value
  4. If it is not a ResponseStatusException type, it depends on whether the exception class has a ResponseStatus annotation. If so, take the reason attribute of the annotation as the return value
  5. If the reason obtained through the annotation is also invalid, the getMessage field of the exception is returned
  • The above content is the essence of this article, but it does not contain the analysis process. If you are interested in Spring Cloud source code, please allow Xin Chen to accompany you for a brief source reading tour.

Spring Cloud Gateway error handling source code

  • The first thing to look at is the configuration class ErrorWebFluxAutoConfiguration.java. Here, two instances are registered for spring, each of which is very important. Let's focus on the first one first, that is, the implementation class of ErrorWebExceptionHandler is DefaultErrorWebExceptionHandler:

  • When handling an exception, it will call the handle method of this ErrorWebExceptionHandler through FluxOnErrorResume. This method is in its parent class AbstractErrorWebExceptionHandler.java, as shown in the following figure. The code in the red box is the key, and the return content of the exception is determined here:

  • Expand the getRoutingFunction method. It can be seen that renderErrorResponse will be called to process the response:
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
		return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);
	}
  • Open the renderErrorResponse method, as shown below. The truth is revealed!
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
  // Remove all error messages
  Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
  
  // Construct all information returned 
  return ServerResponse
           // Control return code
           .status(getHttpStatus(error))
           // Control return ContentType
           .contentType(MediaType.APPLICATION_JSON)
           // Control return content
           .body(BodyInserters.fromValue(error));
}
  • Through the above code, we get two important conclusions:
  1. The status code returned to the caller depends on the return value of the getHttpStatus method
  2. The body returned to the caller depends on the content of error
  • After reading this, you should naturally look at the internal of getHttpStatus, as shown below. status comes from the input parameter:
protected int getHttpStatus(Map<String, Object> errorAttributes) {
  return (int) errorAttributes.get("status");
}
  • So far, we can draw a conclusion: the return value of getErrorAttributes method is the key to determine the return code and return body!

  • Let's take a look at the truth of the getErrorAttributes method. In DefaultErrorAttributes.java (recall that when I saw ErrorWebFluxAutoConfiguration.java just now, I mentioned that everything in it is very important, including the errorAttributes method):

public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = this.getErrorAttributes(request, options.isIncluded(Include.STACK_TRACE));
        if (Boolean.TRUE.equals(this.includeException)) {
            options = options.including(new Include[]{Include.EXCEPTION});
        }

        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }

        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }

        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.put("message", "");
        }

        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }

        return errorAttributes;
    }
  • Due to space constraints, we will not expand the above code, but directly report the results:
  1. The return code is from determineHttpStatus
  2. The message field is returned from determineMessage
  • Open the determineHttpStatus method and the final answer will be revealed. Please pay attention to the Chinese Notes:
private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
        // Is the exception object of type ResponseStatusException
        return error instanceof ResponseStatusException 
        // If it is the ResponseStatusException type, the getStatus method of the exception object is called as the return value
        ? ((ResponseStatusException)error).getStatus() 
        // If it is not a ResponseStatusException type, see if the exception class has a ResponseStatus annotation,
        // If so, take the code attribute of the annotation as the return value
        : (HttpStatus)responseStatusAnnotation.getValue("code", HttpStatus.class)
        // If the exception object is neither ResponseStatusException type nor ResponseStatus annotation, 500 is returned
        .orElse(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  • In addition, the content of the message field also determines:
    private String determineMessage(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
        // Is the exception object of type BindingResult
        if (error instanceof BindingResult) {
            // If so, use getMessage as the return value
            return error.getMessage();
        } 
        // If it is not BindingResult type, it depends on whether it is ResponseStatusException type
        else if (error instanceof ResponseStatusException) {
            // If so, use getReason as the return value
            return ((ResponseStatusException)error).getReason();
        } else {
            // If it is not of ResponseStatusException type,
            // It depends on whether the exception class has a ResponseStatus annotation. If so, take the reason attribute of the annotation as the return value
            String reason = (String)responseStatusAnnotation.getValue("reason", String.class).orElse("");
            if (StringUtils.hasText(reason)) {
                return reason;
            } else {
                // If the reason obtained through the annotation is also invalid, the getMessage field of the exception is returned
                return error.getMessage() != null ? error.getMessage() : "";
            }
        }
    }
  • So far, the source code analysis has been completed. I believe you should know how to control the final return code and return content. In the next practical article, let's strike while the iron is hot and write code to try to accurately control the return code and return content

  • Spoilers in advance, the following content will be presented in the next practical part:

  1. Directly, control the return code and the error field in the body
  2. Small stumbling block, see the move
  3. It is easy to use and returns information through annotation control
  4. The ultimate solution, fully customized return content
  • Please look forward to the above content. Xinchen's original will live up to you

You're not alone. Xinchen's original accompanies you all the way

  1. Java series
  2. Spring collection
  3. Docker series
  4. kubernetes series
  5. Database + middleware series
  6. DevOps series

Welcome to the official account: programmer Xin Chen

Wechat search "programmer Xinchen", I'm Xinchen, looking forward to traveling with you in the Java World
https://github.com/zq2599/blog_demos

Posted on Wed, 24 Nov 2021 19:42:44 -0500 by mcdsoftware