I Broke My SPA Fallback by Renaming a FastAPI Parameter to Satisfy a Linter
I Broke My SPA Fallback by Renaming a FastAPI Parameter to Satisfy a Linter
我为了满足 Linter 而重命名了 FastAPI 参数,结果搞崩了 SPA 回退功能
The dashboard SPA fallback was returning 422 on every route that was not an API route. FastAPI kept asking for full_path as a query param. The handler declared _full_path. Same thing, I assumed. Not the same thing.
仪表盘的 SPA 回退功能在所有非 API 路由上都返回 422 错误。FastAPI 一直要求将 full_path 作为查询参数传入,而我的处理函数声明的是 _full_path。我以为这两者是一回事,但事实并非如此。
FastAPI’s path parameter injection requires the function parameter name to match the path template exactly. My route was /{full_path:path} and the handler had _full_path. Python treats those as different identifiers. FastAPI tried to inject full_path from the path, found no matching parameter, fell back to looking for it as a query string, found nothing, and returned 422. Every React route except / was dead.
FastAPI 的路径参数注入要求函数参数名必须与路径模板完全匹配。我的路由是 /{full_path:path},但处理函数用的是 _full_path。Python 将它们视为不同的标识符。FastAPI 尝试从路径中注入 full_path,却找不到匹配的参数,于是回退到查询字符串中查找,结果依然没找到,最终返回了 422。除了 / 之外,所有的 React 路由全都失效了。
The fix is one character: rename _full_path to full_path. Why was it _full_path in the first place? I had run the linter and my editor suggested prefixing unused parameters with underscores. The parameter was “unused” in the sense that the handler ignores its value and returns index.html regardless of what path comes in. The linter was technically correct and practically wrong. I applied the suggestion without thinking about whether FastAPI cared about the name.
修复方法只需改动一个字符:将 _full_path 重命名为 full_path。为什么最初会是 _full_path 呢?因为我运行了 Linter,编辑器建议给未使用的参数加上下划线前缀。从处理函数会忽略该值并始终返回 index.html 的角度来看,这个参数确实是“未使用的”。Linter 在技术上是正确的,但在实践中却是错误的。我盲目采纳了建议,却没考虑 FastAPI 是否在意参数名称。
The second bug was a port conflict. The server was on 8765, which outreach_ui.py had already claimed. On a cold boot both processes race. The dashboard started second, failed to bind, failed silently, and I spent time wondering why the frontend was dead before checking what was actually listening on 8765. The fix: move to 8766. One digit. No more race.
第二个 Bug 是端口冲突。服务器原本运行在 8765 端口,但 outreach_ui.py 已经占用了它。冷启动时,两个进程会发生竞争。仪表盘进程启动较晚,绑定失败且没有任何报错提示。我浪费了时间去排查前端为什么挂掉,最后才检查到底是什么程序在监听 8765 端口。修复方法:改到 8766。只改了一个数字,竞争不再发生。
What I would do differently: Keep a port registry. There is no file in this monorepo that lists which process owns which port. It is a monorepo with several engines and several UIs and I am already on 8766 after colliding with 8765. Both ports were picked incrementally under the assumption that I would remember. I did not remember. A single ports.md or even a comment block at the top of each server entry point would have made this a five-second check instead of a debugging session.
如果重来一次,我会这样做:维护一个端口注册表。在这个单体仓库(monorepo)中,没有任何文件记录哪个进程占用了哪个端口。仓库里有多个引擎和多个 UI,在与 8765 冲突后,我已经改到了 8766。这两个端口都是凭感觉递增选取的,我以为自己能记住,结果并没有。如果有一个 ports.md 文件,或者在每个服务器入口点顶部加一个注释块,这本可以是一个五秒钟就能确认的问题,而不是一场耗时的调试。
On the FastAPI issue: stop applying linter suggestions at framework boundaries without reading the framework contract. The underscore convention for unused parameters is sensible in normal Python. Route handlers are not normal Python. When FastAPI injects path params by name, “unused from Python’s perspective” is completely different from “unused from FastAPI’s perspective.” The right move is a # noqa comment or a type annotation that satisfies the linter, not a rename that silently severs injection. The linter saw a smell that was not a smell. I trusted it anyway.
关于 FastAPI 的问题:在框架边界处,不要在未阅读框架契约的情况下盲目采纳 Linter 的建议。对于普通 Python 代码,未使用的参数加下划线是一种合理的约定,但路由处理函数并非普通的 Python 代码。当 FastAPI 按名称注入路径参数时,“从 Python 角度看未使用的参数”与“从 FastAPI 角度看未使用的参数”完全是两码事。正确的做法是使用 # noqa 注释或类型注解来满足 Linter,而不是通过重命名来悄无声息地切断注入逻辑。Linter 发现了一个“代码异味”,但那其实并不是异味,而我却盲目地信任了它。
Neither fix was clever. The dashboard works now. Every SPA route hands off to index.html, React Router takes it from there, and nothing is fighting over port 8765 anymore. Two bugs, two single-line fixes, one pattern: automation told me something was wrong and I acted without reading the local contract first. Worth noticing.
这两个修复都不算高明。现在仪表盘恢复正常了:每个 SPA 路由都会交给 index.html,后续由 React Router 接管,也没有程序再争抢 8765 端口了。两个 Bug,两次单行修复,总结出一个模式:自动化工具提示我有问题,而我没先阅读本地契约就直接采取了行动。这一点值得警惕。