简体   繁体   中英

Spring Cloud Gateway - custom 404 error message when route is not found

I have a basic Spring Cloud Gateway app that is configured with YAML:

server:
  port: 8080

logging:
  level:
    reactor:
      netty: INFO
    org:
      springframework:
        cloud:
          gateway: TRACE

spring:
  cloud:
    gateway:
      httpclient:
        wiretap: true
      httpserver:
        wiretap: true
      routes:
        - id: test-route-1
          uri: https://www.google.com
          predicates:
            - Path=/
        - id: test-route-2
          uri: http://1.1.1.1:1111
          predicates:
            - Path=/common/**

Here's the entry point:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The issue I have is that when I try to hit a non-existent route definition with a basic GET request through Postman, for example

localhost:8080/asd

I get back a basic 404 error in Postman:

{
    "timestamp": "2022-04-28T09:27:36.647+00:00",
    "path": "/asd",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "requestId": "86bc902a-8"
}

I want to modify this response and add a meaningful message, such as "Route definition not found." , but it seems like this response is sent even before any of the filters in my gateway configuration are executed. Here's how the console looks when I execute a valid request:

2022-04-19 13:21:48.577  INFO 64240 --- [           main] com.test.apigateway.Application        : Started Application in 11.143 seconds (JVM running for 11.902)
2022-04-19 13:21:50.125 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.f.WeightCalculatorWebFilter      : Weights attr: {}
2022-04-19 13:21:50.145 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "/" matches against value "/"
2022-04-19 13:21:50.146 DEBUG 64240 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: test-route-1
2022-04-19 13:21:50.146 DEBUG 64240 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : Mapping [Exchange: GET http://localhost:8080/] to Route{id='test-route-1', uri=https://www.google.com:443, order=0, predicate=Paths: [/], match trailing slash: true, gatewayFilters=[], metadata={}}
2022-04-19 13:21:50.146 DEBUG 64240 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : [9b8150f7-1] Mapped to org.springframework.cloud.gateway.handler.FilteringWebHandler@5bf45d2c
2022-04-19 13:21:50.148 DEBUG 64240 --- [ctor-http-nio-3] o.s.c.g.handler.FilteringWebHandler      : Sorted gatewayFilterFactories: [[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@1e40fbb3}, order = -2147483648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@7ec08115}, order = -2147482648], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6d5f4900}, order = -1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@1e6060f1}, order = 0], [GatewayFilterAdapter{delegate=com.fadata.apigateway.filters.AuthenticationFilter@5885a768}, order = 1], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@1b560eb0}, order = 10000], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@2c6c302f}, order = 10150], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@7e49ded}, order = 2147483646], GatewayFilterAdapter{delegate=com.fadata.apigateway.Application$LoggingGlobalFiltersConfigurations$$Lambda$525/0x00000008004f6840@51ba952e}, [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@2416c658}, order = 2147483647], [GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@9e02f84}, order = 2147483647]]
2022-04-19 13:21:50.156  INFO 64240 --- [ctor-http-nio-3] c.f.a.filters.AuthenticationFilter       : 1 - Global Pre Filter executed [AuthenticationFilter]
2022-04-19 13:21:50.157 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.filter.RouteToRequestUrlFilter   : RouteToRequestUrlFilter start
2022-04-19 13:21:51.145 TRACE 64240 --- [ctor-http-nio-3] o.s.c.gateway.filter.NettyRoutingFilter  : outbound route: 154ff117, inbound: [9b8150f7-1] 
2022-04-19 13:21:51.243  INFO 64240 --- [ctor-http-nio-3] ation$LoggingGlobalFiltersConfigurations : Global Post Filter executed
2022-04-19 13:21:51.243 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.filter.NettyWriteResponseFilter  : NettyWriteResponseFilter start inbound: 154ff117, outbound: [9b8150f7-1]

It clearly executes both my global pre and post filters before the response hits the client. However, if I send a request to a non-existent route, here's what I see:

2022-04-19 13:23:16.926 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.f.WeightCalculatorWebFilter      : Weights attr: {}
2022-04-19 13:23:16.926 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "[/]" does not match against value "/asd"
2022-04-19 13:23:16.927 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.h.p.PathRoutePredicateFactory    : Pattern "[/common/**]" does not match against value "/asd"
2022-04-19 13:23:16.927 TRACE 64240 --- [ctor-http-nio-3] o.s.c.g.h.RoutePredicateHandlerMapping   : No RouteDefinition found for [Exchange: GET http://localhost:8080/asd]

It seems like it doesn't even execute any filters, so I can't intercept the response and check or modify it in any way.

I've searched within the source code and found where the No RouteDefinition found error is being logged, but I can't intercept the response anywhere it seems.

Just to clarify - I don't need to render an HTML page at all, I just want to modify the JSON object before it is sent back to the client with the 404 response, and add a message to that JSON.

Any ideas how I can do that? Thanks!

I have found a temporary workaround to deal with this issue, but it is very specific and will not work in all cases. Essentially what I have done is I've made a @Component class that extends the DefaultErrorAttributes , and have given implementation to its getErrorAttributes method. This way, I gain access to the response in case any error is encountered in the API Gateway. Here's how the method looks:

@Override
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
    Throwable error = super.getError(request);
    Map<String, Object> map = super.getErrorAttributes(request, options);

    int statusCode = (int) map.get("status");
    HttpStatus status = getHttpStatusFromStatusCode(statusCode);

    map.put("message", populateMapWithErrorMessage(error, map, status));
    return map;
}

Now, in my case all of the services hidden behind the gateway, without exception, have a particular and uniform response body in case an exception occurs, such as hitting a non-existent endpoint, or supplying incorrect query parameters, etc.

However, when an incorrect route is hit, the error comes from the gateway itself, and the body, by default, contains an attribute called requestId . This attribute doesn't exist in the response bodies of any of my underlying services, so I figured I could use that to my advantage.

In my populateMapWithErrorMessage method I check if the error is caused by no route definition found or not, and if it is, I populate the message with the necessary string:

private boolean isErrorCausedByNoRouteDefinitionFound(Throwable error, Map<String, Object> map) {
    return map.containsKey("requestId") && error.getMessage().contains("404");
}

So now whenever I hit a non-existent route, I get back a body that looks like this:

{
    "timestamp": "2022-04-30T15:59:38.016+00:00",
    "path": "/invalid-route",
    "status": 404,
    "error": "Not Found",
    "message": "No route definition with path '/invalid-route' found.",
    "requestId": "ad1f42b1-1"
}

Which is exactly what I needed. Before that custom workaround, the message attribute was null .

Even though this should work for now, I still want to understand how I can manipulate the response directly by detecting if the error is caused by no route definition found. There has to be an easier way, and if anyone knows it I'll be grateful if they could share that.

Here's the entire class for reference:

import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;

import java.util.Arrays;
import java.util.Map;
import java.util.NoSuchElementException;

@Component
public class GatewayErrorAttributes extends DefaultErrorAttributes {
    public static final String ROUTE_DEFINITION_NOT_FOUND_ERR = "No route definition with path '%s' found.";
    public static final String INVALID_STATUS_CODE_ERR = "No HttpStatus corresponds to status code %d";

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
        Throwable error = super.getError(request);
        Map<String, Object> map = super.getErrorAttributes(request, options);

        int statusCode = (int) map.get("status");
        HttpStatus status = getHttpStatusFromStatusCode(statusCode);

        map.put("message", populateMapWithErrorMessage(error, map, status));
        return map;
    }

    private HttpStatus getHttpStatusFromStatusCode(int statusCode) {
        return Arrays.stream(HttpStatus.values())
                .filter(v -> v.value() == statusCode)
                .findFirst()
                .orElseThrow(() -> new NoSuchElementException(String.format(INVALID_STATUS_CODE_ERR, statusCode)));
    }

    private String populateMapWithErrorMessage(Throwable error, Map<String, Object> map, HttpStatus status) {
        return isErrorCausedByNoRouteDefinitionFound(error, map) ?
                String.format(ROUTE_DEFINITION_NOT_FOUND_ERR, map.get("path")) :
                trimStatusAndQuotesFromErrorMessage(error.getMessage(), status);
    }

    private boolean isErrorCausedByNoRouteDefinitionFound(Throwable error, Map<String, Object> map) {
        return map.containsKey("requestId") && error.getMessage().contains("404");
    }

    private String trimStatusAndQuotesFromErrorMessage(String errorMessage, HttpStatus status) {
        return errorMessage
                .replace(status.toString(), "")
                .replace("\"", "")
                .trim();
    }
}

It's pretty simple, you can create a custom html file into resources/public/error/404.html and add your custom error page in there.

Here's the documentation reference.

https://docs.spring.io/spring-boot/docs/2.0.0.M6/reference/html/boot-features-developing-web-applications.html#boot-features-webflux-error-handling-custom-error-pages

<html>
    <head><title>Error 404</title></head>
    <body>Unfortunately we couldn't find this route</body>
</html>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM