背景:我在 WorkBuddy 工作区里维护着一套每天定时跑的自动化脚本——签到、发文、上架、巡查,一共七八个独立脚本,各自带自己的浏览器实例和本地服务。10 月 4 日我发现一个"代理偷走本机请求"的坑并修好了;10 月 6 日它又出现了;10 月 7 日我再看到它时,已经是第三次。这篇记录的就是这三次之间到底漏了什么。
起因很简单:脚本连本机调试端口取标签页,报 502 Bad Gateway。第一反应是浏览器挂了,但手动 curl http://127.0.0.1:9333/json/version 明明是好的。
打印出完整异常才看明白——请求根本没到本机,是被发给了代理:
HTTP_PROXY=http://127.0.0.1:54344
NO_PROXY= # 空的环境里注入了代理变量,而 NO_PROXY 是空的,于是 urllib 把发往 127.0.0.1 的请求也丢给了代理;代理再回环转发失败,就吐了 502。更烦的是代理偶尔还吐缓存响应,制造出"刚才明明通了"的假象,让人在错误的路上排查更久。
当时的修法很直接——给本地请求套一个"不走代理"的 opener:
import urllib.request
# 所有本机回环请求一律走它,绕开 http_proxy
_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def local_get(url, timeout=5):
return _OPENER.open(url, timeout=timeout).read()改完,脚本好了。我把结论写进笔记,收工。
两天后,同类脚本又在另一条线崩了,报错还是 502。查了一下——这个脚本里没有那段修复代码。当初我只改了正在报错的那一个文件。
这是第一个教训:「修过」等于改了一处,不等于修完。 同一个根因散布在多个文件里时,只修触发报错的那个,等于把坑留着等下一次触发。
于是这一轮我把修法做成了共享函数,并在整个项目里回灌:
grep -rn "urllib.request.urlopen" tools/ | grep -v _OPENER只要有输出,就说明还有漏网的直接调用。
第三次的报错和代理完全无关,反而更像"玄学":脚本先打印
browser: 复用已开实例 9335紧跟着就 ConnectionRefused。日志自相矛盾——它自己说用的是 9335,却去连了别的端口。
把代码翻出来就明白了:
DEBUG_PORT = 9333 # 模块级常量
def ensure_browser():
global PORT
PORT[0] = pick_alive_port() # 判定出真正可用的端口,写进了 PORT[0]
def pick_tab():
return http_get(f"http://127.0.0.1:{DEBUG_PORT}/json") # 这里却用了常量判定端口和连接端口用了两个变量。修法是把"一次判定、多处使用"的值收敛到唯一来源:
PORT = [DEBUG_PORT] # 可变容器,全流程唯一真相源
def ensure_browser():
PORT[0] = pick_alive_port()
def pick_tab():
return local_get(f"http://127.0.0.1:{PORT[0]}/json")写下这三次,不是为了抱怨,而是因为它们其实是一类问题:
127.0.0.1,localhost,::1 放进 NO_PROXY。这是环境特性,不是代码 bug,但它会伪装成代码 bug。grep 的成本,远低于再花一晚排查同一个根因。三条纪律落地后,这套自动化连续跑了一周没再出同类故障。回头看,真正花掉时间的不是"怎么修",而是"为什么同一个东西要修三次"——修的是症状,回灌的才是修复。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。