Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
最近做网关 Spring Cloud Gateway功能开发,联调的时候,把本地网关代理出去了,遇到了一个令人困惑的问题:为什么同一个接口,用 IP 形式访问就正常,而换成域名就直接返回 404 Not Found?
这个问题抽象出来是这样的:
-
IP 形式: http://localhost:9900/api-user/xxxx1
这个接口能正常调用,路由成功,毫无问题。
-
域名形式: xxx.xxx.xxx/:9900/api-u...
这个接口不行,网关直接返回 404,路由失败。
本地用 curl
模拟调用,无论是 IP 还是域名,都能正常转发。按理来说域名只是加了个代理转发,网关能够收到域名过来的请求,就正常的。这次分析了 Spring Cloud Gateway 的源码,一探究竟。
源码追踪:追查 404
的元凶
我的追查从 Spring WebFlux 的核心分发器 DispatcherHandler
开始。
DispatcherHandler#handle
:请求分发入口
DispatcherHandler
负责将传入的 ServerWebExchange
(代表一个 HTTP 请求)分发给正确的处理器(Handler)。
Java
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return handlePreFlight(exchange);
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}

这段代码采用了响应式编程的链式调用,依次执行逻辑:
查找 Handler: Flux.fromIterable(this.handlerMappings)
将所有注册的**HandlerMapping
**(如 RoutePredicateHandlerMapping
、SimpleUrlHandlerMapping
等)放入一个响应式流中。concatMap
会顺序地调用每个mapping
的getHandler()
方法,尝试找到能够处理该请求的Handler。
接下来,我将目光锁定在了 Spring Cloud Gateway 自己的 Handler 上。
RoutePredicateHandlerMapping#getHandlerInternal
:路由匹配核心
作为 Gateway 路由匹配的核心,RoutePredicateHandlerMapping
的主要任务就是根据配置的路由谓词,为请求找到对应的 Route
。
RoutePredicateHandlerMapping实现了getHadnlerInternal方法,整体是一个模版设计模式,某些细节下放到子类实现。
Java
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// ...
return lookupRoute(exchange)
.flatMap((Function<Route, Mono<?>>) r -> {
// ... 找到路由后返回 FilteringWebHandler
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
})));
}
java
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
// instead of immediately stopping main flux due to error, log and
// swallow it
.doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e))
.onErrorResume(e -> Mono.empty()))
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.<Route>empty().log("noroute"))
.next()
// TODO: error handling
.map(route -> {
if (logger.isDebugEnabled()) {
logger.debug("Route matched: " + route.getId());
}
validateRoute(route, exchange);
return route;
});
/*
* TODO: trace logging if (logger.isTraceEnabled()) {
* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }
*/
}
源码解读:
lookupRoute(exchange)
:这是查找路由的核心方法。它会遍历所有已定义的路由,并用每个路由的谓词去判断是否与当前请求匹配。.flatMap(...)
: 如果lookupRoute
找到了Route
,就会返回一个Mono.just(webHandler)
,这里的webHandler
就是FilteringWebHandler
,用于执行路由过滤器链。.switchIfEmpty(...)
: 如果lookupRoute
没有找到任何匹配的路由,它会返回一个空的Mono
,从而导致getHandlerInternal
方法也返回Mono.empty()
,即它无法处理该请求。
RoutePredicateHandlerMapping
因为没有匹配到任何路由,无法返回有效的 FilteringWebHandler
。 接下来,我们看遍历route,然后执行谓词匹配
GatewayPredicate#test
:
我继续追踪,最终定位到了路由谓词判断的核心方法:org.springframework.cloud.gateway.handler.predicate.GatewayPredicate#test
。
Java
ini
public boolean test(ServerWebExchange exchange) {
PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath());
PathPattern match = null;
for (PathPattern pathPattern : pathPatterns) {
if (pathPattern.matches(path)) {
match = pathPattern;
break;
}
}
// ...
return match != null;
}
源码解读:
exchange.getRequest().getURI().getRawPath()
: 谜底就在这里! 这行代码用于获取请求的 URI 路径。getRawPath()
方法获取的是未解码的、原始的路径字符串,这对于路径匹配至关重要。pathPattern.matches(path)
: 这里就是进行路由匹配的最终判断,将从getRawPath()
获取的路径与路由规则进行对比。
核心问题就出在 getRawPath()
的返回值上。
IP 形式的请求:http://localhost:9900/api-user/xxxx1
getRawPath() 得到 /api-user/xxxx1。该路径与我们配置的路由规则完美匹配,test 方法返回 true,路由成功。
域名形式的请求:xxx.xxx.xxx/:9900/api-u...
这里的 URL 格式不规范,在主机名和端口号之间多了一个斜杠 /。根据 URI 规范,端口号后面不应有斜杠。当 Java 的 URI 解析器遇到这个不规范的格式时,它会将 :9900/ 当作路径的一部分,而端口视为80,
因此,getRawPath()
返回 /:9900/api-user/xxxx1
。看到这个变量,长的非常奇怪,一下子问题点就发现了,这个不正确的路径自然无法匹配 /api-user/**
的路由规则以及任意其他的路由规则,test
方法返回 false
,路由失败。 无法走网关的匹配逻辑,就无法使用RoutePredicateHandlerMapping的FilteringWebHandler
后续流程交给SimpleUrlHandlerMapping的ResourceWebhandler
的逻辑,同样的在ResourceWebHandler
里面也找不到:9900/api-user/xxxx1
路径。 这个ResourceWebHandler
是在
- META-INF/resources/
- resources/
- static/
- public/
这些地方找静态资源,导致无法找到资源,从而404。接口响应的404也是在这里返回的。

总结:问题的完整链路
- URL 格式不规范: 域名请求
http://xxx.xxx.xxx/:9900/
多了一个不该存在的斜杠。 - 路径解析错误:
getRawPath()
将:9900/
解析成了路径的一部分,导致路径变为/:9900/api-user/xxxx1
。 - 路由匹配失败: 错误的路径无法匹配任何路由规则,
GatewayPredicate#test
返回false
。 - Handler 寻找失败:
RoutePredicateHandlerMapping
因为没有找到匹配的路由,所以无法返回FilteringWebHandler
。 - 请求被
ResourceWebHandler
接管: 鉴于请求没有找到任何业务 Handler,Spring Boot 默认配置中的SimpleUrlHandlerMapping
会将请求交给ResourceWebHandler
处理,期望它能找到静态资源。 - 最终返回 404: 然而,在静态资源目录下,自然找不到
/:9900/api-user/xxxx1
这个文件。ResourceWebHandler
同样无法处理该请求,最终导致DispatcherHandler
抛出404 Not Found
错误。
最终结论与解决方案
问题的根源在于域名 URL 格式不规范 ,导致网关获取到错误的请求路径,进而无法匹配任何路由,最终返回 404 Not Found
错误。
- 根本解决方案: 从源头修改请求 URL 格式,确保其符合规范,即
http://xxx.xxx.xxx:9900/api-user/xxxx1
。 - 临时解决方案(不推荐): 如果无法修改前端,可以在网关内部编写自定义的全局过滤器(
GlobalFilter
),在路由匹配之前,手动修正这个不规范的路径。
这次排查不仅解决了实际问题,也让我深刻理解了 URI 规范的重要性,以及 Spring Cloud Gateway 路由匹配的底层机制。在未来,遇到类似的路由问题时,我们应该首先检查请求的原始路径是否符合预期。