记一次因为nginx配置不当,导致Vite离奇报错URI malformed
前言
我有个爱好,喜欢在闲余时间给自己写一些可以满足强迫症/收集癖/管理欲的工具。
例如我喜欢刷图收集图片,就做了个图片管理工具;我玩手游,就做了个给手游写个简单的数据展示,管理抽卡记录,等等。事情就发生我开发这些工具的时候……
(注意:事情发生在自家电脑的私人项目中,没有任何规范和兼容可言)
关于工具
我得先把工具的情况介绍一下
- 统一用
HTML网页作为用户界面 - (重点)统一用
Vite打包 - (重点)统一用
nginx映射到localhost域名。举例:- 图片工具,B/S架构,监听
27311端口,nginx配置proxy_pass到image.localhost; - 手游工具,静态网页,
nginx配置root到game.localhost;
- 图片工具,B/S架构,监听
发生了啥?
当时我玩的手游即将更新版本,循例打开手游工具的项目,更新基础数据,顺便调整样式。运行了Vite进行网页网页。
就在调试网页打开不久后,Vite突然弹窗报错:
URI malformed
这个错误在平时开发中并不少见,简单来说就是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、调试错误堆栈
虽然定位了有问题的端口,但是从端口定义中找不到有用的信息。那就换个地方排查,最有可能获取信息的地方:图中的错误堆栈。
打开从堆栈最上面的代码,定位对应行,代码一目了然。
(这就是脚本语言最独特的优势之一了,可以直接看到和修改调试第三方库的源代码)
可以直接从代码看出的信息:
- 首先一眼明白这是一段处理url的代码,用到
decodeURI函数来对URL解码。很明显URI malformed就是从decodeURI抛出的,这是它可能抛出的错误; - 这几行代码在一个叫
viteTransferMiddleware中间件函数中。函数的参数分别是req,res,next。用过相关库的人都能反应过来,这是Express.js的处理请求“三幻神”。(原来Vite用Express.js当服务器呀)
开始调试:
- 在代码处理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
- 继续观察一段时间
确认所有错误请求的路径都是
/announce。
- 进一步排除这个网址的参数
确认导致解码错误的参数是最前面的
info_hash和peer_id。
- 检查浏览器的开发工具中的请求,没有发现这个请求。
基本确认这个请求来自其他程序(
Vite内部也有可能,但直觉告诉我可能性很低)
3、追踪请求
(当时看到这请求的时候,就觉得好生熟悉,但一时又想不起来。)
要追踪请求,第一步就是去检查完整的地址,去确认它是通过域名还是ip+端口请求的。于是在上面的调试,我额外打印了它的请求头中的origin和host。结果分别是undefined和127.0.0.1:7300。
- 请求头
origin为空,说明很可能是从程序发出的,而不是常规的浏览器; - 请求头
host是127.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。转发时不会保留origin、host这些请求头。所以如果有一个Tracker的域名指向127.0.0.1,端口是80,这样的nginx确实会接收到请求。
不过这是看上去最不可能的情况。因为我为每个工具都设置了对应的server_name,即使nginx接收了,也不会转发才对。
7300端口对应的server_name是dev.localhost、*.dev.localhost。总不能真有Tracker服务器刚好叫xxx.dev.localhost吧。
但既然目光已经转到nginx上了,那就是去看看日志吧:
艰苦地打开了两年多没处理的180多MB的access.log,搜索/announce。好家伙!真有!
由于这个日志是默认的,没有显示$host。于是我赶紧移走日志,指定格式,重启nginx。再看日志:
一目了然
好嘛,ipv4.tracker.harry.lu这个域名指向了127.0.0.1,nslookup也证实了。真给nginx接收了,我去
修复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,这下就对了:
结语
从Vite到BitComet再Nginx,居然能形成这样的连锁反应,真特么的巧合,服了