记一次因为nginx配置不当,导致Vite离奇报错URI malformed

1,146 阅读9分钟

记一次因为nginx配置不当,导致Vite离奇报错URI malformed

前言

我有个爱好,喜欢在闲余时间给自己写一些可以满足强迫症/收集癖/管理欲的工具
例如我喜欢刷图收集图片,就做了个图片管理工具;我玩手游,就做了个给手游写个简单的数据展示,管理抽卡记录,等等。事情就发生我开发这些工具的时候……

(注意:事情发生在自家电脑的私人项目中,没有任何规范和兼容可言)

关于工具

我得先把工具的情况介绍一下

  • 统一用HTML网页作为用户界面
  • (重点)统一用Vite打包
  • (重点)统一用nginx映射到localhost域名。举例:
    • 图片工具,B/S架构,监听27311端口,nginx配置proxy_pass到image.localhost
    • 手游工具,静态网页,nginx配置root到game.localhost

发生了啥?

当时我玩的手游即将更新版本,循例打开手游工具的项目,更新基础数据,顺便调整样式。运行了Vite进行网页网页。
就在调试网页打开不久后,Vite突然弹窗报错:

URI malformed

QQ截图20240620141422.png 这个错误在平时开发中并不少见,简单来说就是URL解码失败。

但是!这个错误Vite本身是很少见,一般没理由报这个错误的

当时我首先想的是:

  • 首先这是纯网页的项目,需要东西都通过Vite处理,不会涉及URL请求的;
  • 其次这错误以前没见过,哪怕在自己从头搭框架制定结构的时候,也没见过;
  • 这次开发只是修改数据和调整样式,都是框架内的调整。需要动态import的文件连unicode字符都没有,根本不需要URL编码。而且文件也没有新增/删除/改名过。

所以我认为,这不是我的代码造成。

排查

虽然只需要保存一下代码,Vite热更新就能让错误弹窗消失。但是莫名其妙的不定时的错误弹窗总是令人烦躁的。还是要排掉它。

0、搜索Vite的GitHub仓库的Issues

结果:近期没有相关的Issue。我安装的版本是最新的,但不是最近几天的新版本。如果是Vite的问题,就该有人报错。

所以感觉不是Vite的问题

1、代码以外的唯一变动

当时要说除了代码还有哪里变了,那就只有一个地方,vite.config.js。因为那天我改进了映射的用nginx配置,要同步更新了Vite的开发用端口。

这里简单补充下我关于开发端口的设计:

  • 改进nginx配置前,手游工具分配到的开发端口是7322,映射到域名就是dev.game.localhost
  • 改进nginx配置后,开发端口是7300,映射到域名就是game.dev.localhost

大致意思是:静态网页的工具,都不再有独立的端口,统一用7300端口,对应域名*.dev.localhost*就是工具各自的名字了。如果同时开发多个,就顺延到7301对应dev1这样。恰好Vite也是这样顺延端口。
73XX不是当时的实际端口。出于我电脑的安全,在文中统一做了修改,但格式是一样的。

测试结果是:

端口URI malformed错误
7299无 ✖
7300有 ✔
7301无 ✖
7322无 ✖
7321无 ✖
7323无 ✖

结果确实只有7300端口会有报错。

但查了7300的端口定义,这端口并不是什么特殊端口。也没有其他程序和Vite抢端口。

2、调试错误堆栈

虽然定位了有问题的端口,但是从端口定义中找不到有用的信息。那就换个地方排查,最有可能获取信息的地方:图中的错误堆栈

打开从堆栈最上面的代码,定位对应行,代码一目了然。
(这就是脚本语言最独特的优势之一了,可以直接看到和修改调试第三方库的源代码)
QQ截图20240620173514.png

可以直接从代码看出的信息:

  1. 首先一眼明白这是一段处理url的代码,用到decodeURI函数来对URL解码。很明显URI malformed就是从decodeURI抛出的,这是它可能抛出的错误;
  2. 这几行代码在一个叫viteTransferMiddleware中间件函数中。函数的参数分别是reqresnext。用过相关库的人都能反应过来,这是Express.js的处理请求“三幻神”。(原来Vite用Express.js当服务器呀)

开始调试:

  1. 在代码处理url前,打印req.url(没截图)。

确认在正常情况下,这段代码就是在处理来自网页的请求。

而且来自网页的请求都是可以正确解码的,没有报错的; 2. 取消上面的打印,改为只在产生错误的地方,打印错误的req.url(如图)。得到产生错误的网址:

/announce?info_hash=%CB%9E%0A%01%12%D9%04%A5%E3%CC%F0%01%D1%BE%8CB%CB%BBu%01&peer_id=-BC0207-%8D%86N%92%E2%CB%F4%7F%BB%A1%8F%92&port=22223&natmapped=1&localip=192.168.X.X&port_type=lan&uploaded=6678626520&downloaded=0&left=2130706432&numwant=50&compact=1&no_peer_id=1&key=14093&event=started

  1. 继续观察一段时间

确认所有错误请求的路径都是/announce

  1. 进一步排除这个网址的参数

确认导致解码错误的参数是最前面的info_hashpeer_id

  1. 检查浏览器的开发工具中的请求,没有发现这个请求。

基本确认这个请求来自其他程序Vite内部也有可能,但直觉告诉我可能性很低)

3、追踪请求

(当时看到这请求的时候,就觉得好生熟悉,但一时又想不起来。)
要追踪请求,第一步就是去检查完整的地址,去确认它是通过域名还是ip+端口请求的。于是在上面的调试,我额外打印了它的请求头中的originhost。结果分别是undefined和127.0.0.1:7300

  • 请求头origin为空,说明很可能是从程序发出的,而不是常规的浏览器;
  • 请求头host127.0.0.1,说明是本机的程序发出的请求。

本机程序发出,又是http请求,可以打开Wireshark监听请求,应该能顺藤摸瓜找到发出的程序。

就在我准备打开Wireshark的时候,我习惯地往右下托盘区瞄了一眼,看到了BitComet的图标。瞬间明白怎么回事了!

/announce请求是BitTorrent协议相关的请求啊!

于是我觉得可能是有Tracker服务器的域名指到了127.0.0.1,然后端口是7300。兴奋地打开BitComet选项检查。但令人失望的是,默认Tracker列表中没有符合的Tracker。那么这个Tracker很可能是一个动态Tracker

追踪有种卡住的感觉。请求肯定是从BT客户端发出的,如果是动态Tracker的话,感觉处理不了。我总不能每次添加一个种子都检查排除一遍吧。开发时关掉BT客户端虽然是个有效的办法,但也只是个治标不治本方法。

4、恍然大悟

请求追踪已经结束,但是无法完美处理源头,多少有点泄气。但本着一股强迫症不爽的劲,继续思考,翻看各处的代码程序。终于让我发现了端倪:

诚然如果一个Tracker的域名指向127.0.0.1,且端口是7300。BT客户端是会向127.0.0.1:7300发送请求的。

但还存在一种可能,那就是通过nginx转发请求!

我电脑的nginx配置的转发只设置了最简单的proxy_pass。转发时不会保留originhost这些请求头。所以如果有一个Tracker的域名指向127.0.0.1,端口是80,这样的nginx确实会接收到请求。

不过这是看上去最不可能的情况。因为我为每个工具都设置了对应的server_name,即使nginx接收了,也不会转发才对。
7300端口对应的server_namedev.localhost*.dev.localhost。总不能真有Tracker服务器刚好叫xxx.dev.localhost吧。

但既然目光已经转到nginx上了,那就是去看看日志吧:
艰苦地打开了两年多没处理的180多MB的access.log,搜索/announce。好家伙!真有!
QQ截图20240620185411.png

由于这个日志是默认的,没有显示$host。于是我赶紧移走日志,指定格式,重启nginx。再看日志:
QQ截图20240620190301.png

一目了然

好嘛,ipv4.tracker.harry.lu这个域名指向了127.0.0.1,nslookup也证实了。真给nginx接收了,我去
QQ截图20240619192737.png

修复nginx配置

既然这个/announce请求真是从nginx转发过来的,那么问题在哪就明显了:

nginx配置不当,错误地转发了本应拒绝的请求

重新检查我的nginx配置,发现两个信息:

  • 7300对应dev.localhost,是第一个server块;
server {
  listen 127.0.0.1:80;
  server_name dev.localhost *.dev.localhost;
  server_name dev0.localhost *.dev0.localhost;

  location / {
    proxy_pass http://127.0.0.1:7300;
  }
}
  • 最后一个是负责回落的通配的server块,但是没生效;
server {
  listen 127.0.0.1:80;
  server_name *.localhost;

  location / {
    deny all;
  }
}

相信熟悉nginx或者眼尖的人已经发现问题所在了:

server_name *.localhost;是不能匹配ipv4.tracker.harry.lu的!

nginx匹配一圈都没有一个server块能处理。所以最终回到起点,让第一个server块处理了

还记得以前设计这配置的时候,思维都在关注二级域名的分配问题上。本能地认为二级域名是*,就是万能的通配。虽然在只限本地情况下,这是正确的。但确实没有覆盖到有公网域名指向127.0.0.1的情况。
(补充一句,明明上俩周才看关于腾讯的网页产品如何获知本地QQ登录状态的文章,原理一样是通过子域名指向127.0.0.1。我居然没有反应过来,真是老了!)

最后的修复也很简单:

  • listen指令添加default_server
  • server_name指令改成完全通配的_;(注意不是**在高版本中已经不支持了)
server {
  listen 127.0.0.1:80 default_server;
  server_name _;

  location / {
    deny all;
  }
}

再次模拟请求,返回403,这下就对了: QQ截图20240620200826.png

结语

从Vite到BitComet再Nginx,居然能形成这样的连锁反应,真特么的巧合,服了