FastAPI ate my SPA routes because I named a param wrong
FastAPI was treating full_path as a required query string parameter instead of capturing it from the URL path. That is not a framework bug. That is a typo that cost me two hours.
Here is what happened. The route was defined as /app/{full_path:path} to catch every URL the React frontend might request and serve back index.html. That is the standard SPA fallback pattern. But the Python handler had _full_path as its parameter name, with an underscore. FastAPI is strict about this: if the path template says {full_path} and your function signature says _full_path, FastAPI does not match them. It infers that full_path must be coming from somewhere else, settles on query string, marks it required, and starts rejecting every request that does not carry ?full_path=... explicitly. Which is every request your browser makes.
The symptom was obnoxious because it looked like a routing config problem. I spent time checking the route order, checking whether the static file mount was shadowing the fallback, checking whether the catch all path pattern was parsed correctly. All of that was fine. The actual problem was one character: the underscore prefix I had probably added to suppress a linter warning about an unused variable.
The fix is one line. Rename _full_path to full_path in the handler signature. Parameter name in the function must match the path variable name in the decorator. FastAPI then captures it from the URL, hands it to the function, and you can ignore it or use it, your choice. The SPA fallback works, every deep link loads, the app behaves like an app.
The second problem in the same commit was a port conflict. Port 8765 was already claimed by outreach_ui.py, another process running on the same machine. The dashboard server would start, bind would fail silently or noisily depending on OS behavior, and you would have two services fighting over the same socket. The fix is also one line: change the port to 8766. No coordination overhead, no dynamic port resolution, just pick a port that is free and write it down.
These are both dumb problems and I want to be honest about that. Neither required architectural insight. Both required reading the error message carefully and not jumping to the wrong diagnosis. The FastAPI one I got wrong twice before I found it because I was looking at the wrong layer of the stack. The port one I only noticed because the bind error was in the logs if I scrolled far enough.
What I would do differently: add an integration smoke test that hits /app/some/deep/path and asserts a 200 with HTML content. That would have caught the param mismatch in CI before I ever ran the server manually. A one line curl in the test suite would have saved two hours of poking at route config.
For the port problem, the better fix is to pull the port into an environment variable with a documented default, so any future conflict is one env var away from being resolved without touching code. Hardcoded ports are fine for personal tooling until the day they are not, and that day always comes when you are in the middle of something else.
Both fixes shipped in one commit. The diff is three changed lines total. The investigation was longer than the fix by an order of magnitude, which is how it usually goes.
Write a comment