chafan-dev / chafan-dev/chafan-core
Server errors reach the browser without CORS headers, so the PWA cannot tell them from a dead network
- Dominant language
- Python
- Stars
- 29
- Forks
- 6
- Avg merge
- 4h 48m
- Merged PRs (30d)
- 10
Description
## Why this is worth fixing
While #207 was live, `PUT /me` failed for every non-null URL value. From the PWA, that failure was **completely opaque**:
```
TypeError: Failed to fetch // fetch
status: 0, event: "error" // XHR
```
No status, no body, nothing to show the user and nothing to log. Indistinguishable from the user's wifi dropping. I only recovered the real status — **503** — by reading Chrome's own network log, which application code cannot do. That single missing header was most of the debugging time on #207, and it will cost the same again on the next server error.
## What I verified
Handled responses are fine. Every one of these carries the header correctly (curl, `Origin: https://cha.fan`, against production):
| request | status | `access-control-allow-origin` |
| --- | --- | --- |
| `GET /api/v1/me` (no auth) | 401 | `https://cha.fan` |
| `PUT /api/v1/me` (no auth) | 401 | `https://cha.fan` |
| `GET /api/v1/nonexistent-xyz` | 404 | `https://cha.fan` |
| `OPTIONS /api/v1/me` (direct) | 405 | `https://cha.fan` |
| `DELETE /api/v1/me` | 405 | `https://cha.fan` |
| `GET /health` | 200 | `https://cha.fan` |
Preflights are fine too: `OPTIONS` with `Access-Control-Request-Method: PUT` returns 200 with `access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT` and the right origin.
So `CORSMiddleware` is correctly configured for anything the app returns through its normal path. The gap is elsewhere.
## Two candidates, and I could not distinguish them
**1. Starlette's `ServerErrorMiddleware` sits above user middleware.** An unhandled exception's 500 is generated *outside* `CORSMiddleware`, so it never gets the header. This is structural and applies to every unhandled exception in the app, which is exactly the class of failure you most need to see. A `@app.exception_handler(Exception)` that returns a real response — so it passes back down through the middleware stack — closes it.
**2. Cloudflare synthesised the 503.** We saw 503, not 500. If the origin died or timed out rather than returning a response, the error page is Cloudflare's and carries none of the app's headers. If that is what happened, the fix is at the edge, not here.
Distinguishing these needs the server log for one of the failing requests: an application traceback with a 500 points at (1); nothing reaching the app, or a worker dying, points at (2).
## Suggested check
The cheapest reproduction is a temporary debug route that raises, called from a browser on `https://cha.fan`:
- If the browser sees a 500 with a body → the header is present and neither theory holds.
- If the browser sees `Failed to fetch` / status 0 → reproduce, then compare `curl -i` (which ignores CORS) to see whether the origin returned 500 or the edge returned 503.
## Correction to what I wrote earlier
PR #209's description says the 503 "never passes back through `CORSMiddleware`", and the `stringify_urls` docstring says the same. Given the table above, that was an inference from the browser's behaviour rather than something I had verified, and it points at the wrong layer. The observation — the browser could not see the failure — stands; the stated cause does not. Worth correcting that docstring when this is settled.
Contributor guide
Research direction
Start with the server logs for a failing request and the temporary debug route described in the issue; compare browser behavior with `curl -i`. Determine whether the 503 is an application error outside `CORSMiddleware` or a Cloudflare-generated response, then update the `stringify_urls` docstring and relevant handling once the source is established.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend, cloud
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 42/100