当前位置: 首页 > news >正文

Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查

Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查

最近做网关 Spring Cloud Gateway功能开发,联调的时候,把本地网关代理出去了,遇到了一个令人困惑的问题:为什么同一个接口,用 IP 形式访问就正常,而换成域名就直接返回 404 Not Found

这个问题抽象出来是这样的:

  • IP 形式: http://localhost:9900/api-user/xxxx1

    这个接口能正常调用,路由成功,毫无问题。

  • 域名形式: http://xxx.xxx.xxx/:9900/api-user/xxxx1

    这个接口不行,网关直接返回 404,路由失败。

本地用 curl 模拟调用,无论是 IP 还是域名,都能正常转发。按理来说域名只是加了个代理转发,网关能够收到域名过来的请求,就正常的。这次分析了 Spring Cloud Gateway 的源码,一探究竟。


源码追踪:追查 404 的元凶

我的追查从 Spring WebFlux 的核心分发器 DispatcherHandler 开始。

DispatcherHandler#handle:请求分发入口

DispatcherHandler 负责将传入的 ServerWebExchange(代表一个 HTTP 请求)分发给正确的处理器(Handler)。

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**(如 RoutePredicateHandlerMappingSimpleUrlHandlerMapping 等)放入一个响应式流中。concatMap会顺序地调用每个mappinggetHandler()方法,尝试找到能够处理该请求的Handler。

接下来,我将目光锁定在了 Spring Cloud Gateway 自己的 Handler 上。

RoutePredicateHandlerMapping#getHandlerInternal:路由匹配核心

作为 Gateway 路由匹配的核心,RoutePredicateHandlerMapping 的主要任务就是根据配置的路由谓词,为请求找到对应的 Route

RoutePredicateHandlerMapping实现了getHadnlerInternal方法,整体是一个模版设计模式,某些细节下放到子类实现。

protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {// ...return lookupRoute(exchange).flatMap((Function<Route, Mono<?>>) r -> {// ... 找到路由后返回 FilteringWebHandlerreturn Mono.just(webHandler);}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");})));
}

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 testingexchange.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

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,路由成功。

域名形式的请求:http://xxx.xxx.xxx/:9900/api-user/xxxx1

这里的 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也是在这里返回的。

在这里插入图片描述


总结:问题的完整链路

  1. URL 格式不规范: 域名请求 http://xxx.xxx.xxx/:9900/ 多了一个不该存在的斜杠。
  2. 路径解析错误: getRawPath():9900/ 解析成了路径的一部分,导致路径变为 /:9900/api-user/xxxx1
  3. 路由匹配失败: 错误的路径无法匹配任何路由规则,GatewayPredicate#test 返回 false
  4. Handler 寻找失败: RoutePredicateHandlerMapping 因为没有找到匹配的路由,所以无法返回 FilteringWebHandler
  5. 请求被 ResourceWebHandler 接管: 鉴于请求没有找到任何业务 Handler,Spring Boot 默认配置中的 SimpleUrlHandlerMapping 会将请求交给 ResourceWebHandler 处理,期望它能找到静态资源。
  6. 最终返回 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 路由匹配的底层机制。在未来,遇到类似的路由问题时,我们应该首先检查请求的原始路径是否符合预期。


文章转载自:

http://MXbaLtrQ.krjyq.cn
http://LwWikqg1.krjyq.cn
http://FMlOXPhK.krjyq.cn
http://EVpnf1xf.krjyq.cn
http://MJmCJSz5.krjyq.cn
http://ehx4BErO.krjyq.cn
http://oiIh2aLv.krjyq.cn
http://ZdtSCF5H.krjyq.cn
http://0xSlDm0E.krjyq.cn
http://usXjjCZj.krjyq.cn
http://1ddxitlx.krjyq.cn
http://uFULoSVx.krjyq.cn
http://J5ZM40AA.krjyq.cn
http://jCurwpzi.krjyq.cn
http://Xq0uOOJ4.krjyq.cn
http://zBddlkce.krjyq.cn
http://TngaiItU.krjyq.cn
http://8a9sExrx.krjyq.cn
http://Cfut4Qaq.krjyq.cn
http://pYPALieg.krjyq.cn
http://VraFBcwr.krjyq.cn
http://dTsvodte.krjyq.cn
http://8jZRjxSM.krjyq.cn
http://46rKnZU2.krjyq.cn
http://Gd6oghqp.krjyq.cn
http://6wPIhBTi.krjyq.cn
http://4QvolneX.krjyq.cn
http://ik13WeNF.krjyq.cn
http://8LMMGs1S.krjyq.cn
http://GMFKnnna.krjyq.cn
http://www.dtcms.com/a/387440.html

相关文章:

  • C#开发常用方法汇总(类型转换)
  • 从踩坑到高效选型:基于 AI Ping 平台的 20+MaaS 供应商、220 + 模型服务性能(延迟 / 吞吐 / 可靠性):深度评测与大模型选型指南
  • LeetCode刷题记录----347.前K个高频元素(Medium)
  • Windows 部署hexo并启动自己的博客
  • 建议对下载的geo原始数据进行低表达基因过滤**,这是数据预处理的关键步骤之一,可提升后续分析(如差异表达、WGCNA)的准确性和可靠性
  • MySQL 数据库备份与恢复
  • SQLite 数据库简介
  • Java进阶教程,全面剖析Java多线程编程,线程的优先级,笔记07
  • YOLOv12目标检测:使用自定义数据集训练 YOLOv12 检测坑洞严重程度
  • 计算机操作系统学习(五、输入输出管理)
  • Rocksteady开发新《未来蝙蝠侠》游戏 有望登陆PS5/PS6
  • Python爬虫实战——使用NetNut网页解锁器获取亚马逊电商数据的入门指南
  • 【 mq】 mq学习笔记
  • 科学研究系统性思维的理论基础:传统研究工具应用
  • Java基础:基本数据类型与变量(详解)
  • VsCode中配置Git-Bash终端
  • 《无人机政务应用视频图像服务成本度量规范》(T/DGAG025-2024)标准解读
  • 2/3维旋转矩阵推导与助记--记录
  • 【代码随想录算法训练营——Day15】二叉树——110.平衡二叉树、257.二叉树的所有路径、404.左叶子之和、222.完全二叉树的节点个数
  • 《从终端到内核:Linux 指令体系的入门与技术解构(第二篇)》
  • 实验5:组件应用(4学时)
  • 精选40道Kafka面试
  • web自动化随笔
  • HarmonyOS 多线程编程:Worker 使用与性能优化指南
  • 卫星通信大爆发:未来,你的手机将不再“失联”
  • 带你了解STM32:EXTI外部中断
  • Charles抓包工具新手入门教程 安装配置、手机代理与基础使用指南
  • 鸿蒙智能设备自动诊断实战:从传感器采集到远程上报的完整实现
  • 第五章 Arm C1-Premium 内存管理单元详解
  • 第七章 Arm C1-Premium L1数据内存系统解析