Add CORS middleware — frontend at pal-e-production hostname cannot fetch API #256

Closed
opened 2026-04-11 20:37:20 +00:00 by forgejo_admin · 1 comment
Contributor

Type

Bug

Lineage

Standalone — discovered during 2026-04-11 session while creating target-architecture and user-story notes for the new westside-emails project. Tried to view them at pal-e-production.tail5b443a.ts.net/notes/<slug> and observed "failed to fetch" in the browser. Root-caused to missing CORS middleware.

Repo

forgejo_admin/pal-e-api

What Broke

Every note detail, board, and list page on pal-e-production.tail5b443a.ts.net fails client-side with "failed to fetch." The nav renders, then every getNote, getNoteBlocks, getNoteToc, getNoteLinks, listNotes call from the browser is blocked by same-origin policy and returns no data.

The API itself is healthy — curl to pal-e-docs.tail5b443a.ts.net/notes/<slug> returns correct JSON with 200 OK. The problem is that the response has zero CORS headers, so browsers discard it on cross-origin requests from pal-e-production.tail5b443a.ts.net.

Root cause: src/pal_e_docs/main.py instantiates FastAPI() without CORSMiddleware. git log -S "CORSMiddleware" returns zero commits — the middleware has never existed in this repo. The bug was hidden while the frontend was Jinja2 server-rendered in-process or reverse-proxied under the same hostname. It surfaced ~7 days ago when the pal-e-production ingress was created as a separate hostname from pal-e-docs.

Repro Steps

  1. Open https://pal-e-production.tail5b443a.ts.net/notes/arch-westside-emails in a browser.
  2. Observe the nav loads but the note body never renders.
  3. Open devtools → Console — see CORS error, or Network → see the /notes/<slug> XHR has no Access-Control-Allow-Origin header and is blocked.
  4. Confirm from a shell with an Origin header:
    curl -s -D- -o /dev/null \
      -H "Origin: https://pal-e-production.tail5b443a.ts.net" \
      https://pal-e-docs.tail5b443a.ts.net/notes/arch-westside-emails
    
    Observe HTTP/2 200, JSON body, no access-control-allow-origin header anywhere in the response headers.
  5. Confirm the middleware has never been added: git log -S "CORSMiddleware" in pal-e-api returns zero results.

Expected Behavior

API responses to cross-origin browser requests from pal-e-production.tail5b443a.ts.net (and http://localhost:5173 for local Vite dev) must include a matching Access-Control-Allow-Origin header so the browser permits the fetch. Preflight OPTIONS requests must return 200/204 with Access-Control-Allow-Methods including GET, POST, PATCH, PUT, DELETE.

Environment

  • Cluster/namespace: pal-e-docs (API) and pal-e-production (frontend)
  • API ingress: pal-e-docs.tail5b443a.ts.net (46d old, uvicorn, FastAPI)
  • Frontend ingress: pal-e-production.tail5b443a.ts.net (7d old, nginx serving SvelteKit SPA, last built 2026-03-28)
  • API repo: forgejo_admin/pal-e-api (renamed from pal-e-docs on 2026-03-27 per issue #217)
  • Frontend repo: pal-e-docs-app (renamed from pal-e-app, checked out locally at ~/pal-e-app)
  • Frontend config: VITE_PAL_E_DOCS_API_URL defaults to https://pal-e-docs.tail5b443a.ts.net

Acceptance Criteria

  • CORSMiddleware added to src/pal_e_docs/main.py before the router includes.
  • allow_origins list is configurable via an env var (e.g. PAL_E_DOCS_CORS_ORIGINS, comma-separated) so non-prod deployments can add origins without a code change.
  • Default origin list (for prod) includes https://pal-e-production.tail5b443a.ts.net and http://localhost:5173.
  • allow_methods includes GET, POST, PATCH, PUT, DELETE, OPTIONS. allow_headers="*". allow_credentials=True only if the frontend sends credentialed requests (confirm against src/lib/api-client.ts which attaches the Keycloak Bearer token).
  • A GET with Origin: https://pal-e-production.tail5b443a.ts.net returns access-control-allow-origin: https://pal-e-production.tail5b443a.ts.net in the response headers.
  • An OPTIONS preflight with Origin: https://pal-e-production.tail5b443a.ts.net and Access-Control-Request-Method: GET returns 200/204 with access-control-allow-methods including GET.
  • New test in tests/ asserting CORS headers on a sample endpoint when Origin is set.
  • Visiting https://pal-e-production.tail5b443a.ts.net/notes/arch-westside-emails in a browser renders the note body without devtools errors.
  • Deployed via the existing CI pipeline (Woodpecker on forgejo_admin/pal-e-api) — no Tofu/kustomize change needed.
  • No regression in curl/SDK access from non-browser callers (they don't care about CORS headers, but confirm the response body shape is unchanged).
  • arch-westside-emails — the note that surfaced the bug when it couldn't be rendered
  • pal-e-docs — pal-e-docs project, owns board-pal-e-docs where this ticket lives
  • forgejo_admin/pal-e-api #217 — the rename from pal-e-docs to pal-e-api
  • forgejo_admin/pal-e-platform#278downstream dependency: hostname swap structural ticket. When the swap lands, the new frontend hostname pal-e-docs.tail5b443a.ts.net must be added to the PAL_E_DOCS_CORS_ORIGINS env var. The env-var-driven design in this ticket makes that a config update, not a code change.
  • src/pal_e_docs/main.py:49 — the line where CORSMiddleware must be added
  • ~/pal-e-app/src/lib/api-client.ts:16 — frontend origin of failing fetches
  • arch:notes-api traceability gap — no backing arch note exists for the API-routes layer; follow-up ticket TBD (accepted as known debt for this ticket)
### Type Bug ### Lineage Standalone — discovered during 2026-04-11 session while creating target-architecture and user-story notes for the new `westside-emails` project. Tried to view them at `pal-e-production.tail5b443a.ts.net/notes/<slug>` and observed "failed to fetch" in the browser. Root-caused to missing CORS middleware. ### Repo `forgejo_admin/pal-e-api` ### What Broke Every note detail, board, and list page on `pal-e-production.tail5b443a.ts.net` fails client-side with "failed to fetch." The nav renders, then every `getNote`, `getNoteBlocks`, `getNoteToc`, `getNoteLinks`, `listNotes` call from the browser is blocked by same-origin policy and returns no data. The API itself is healthy — `curl` to `pal-e-docs.tail5b443a.ts.net/notes/<slug>` returns correct JSON with `200 OK`. The problem is that the response has **zero CORS headers**, so browsers discard it on cross-origin requests from `pal-e-production.tail5b443a.ts.net`. Root cause: `src/pal_e_docs/main.py` instantiates `FastAPI()` without `CORSMiddleware`. `git log -S "CORSMiddleware"` returns zero commits — the middleware has never existed in this repo. The bug was hidden while the frontend was Jinja2 server-rendered in-process or reverse-proxied under the same hostname. It surfaced ~7 days ago when the `pal-e-production` ingress was created as a separate hostname from `pal-e-docs`. ### Repro Steps 1. Open https://pal-e-production.tail5b443a.ts.net/notes/arch-westside-emails in a browser. 2. Observe the nav loads but the note body never renders. 3. Open devtools → Console — see CORS error, or Network → see the `/notes/<slug>` XHR has no `Access-Control-Allow-Origin` header and is blocked. 4. Confirm from a shell with an Origin header: ``` curl -s -D- -o /dev/null \ -H "Origin: https://pal-e-production.tail5b443a.ts.net" \ https://pal-e-docs.tail5b443a.ts.net/notes/arch-westside-emails ``` Observe `HTTP/2 200`, JSON body, **no `access-control-allow-origin` header** anywhere in the response headers. 5. Confirm the middleware has never been added: `git log -S "CORSMiddleware"` in `pal-e-api` returns zero results. ### Expected Behavior API responses to cross-origin browser requests from `pal-e-production.tail5b443a.ts.net` (and `http://localhost:5173` for local Vite dev) must include a matching `Access-Control-Allow-Origin` header so the browser permits the fetch. Preflight `OPTIONS` requests must return `200`/`204` with `Access-Control-Allow-Methods` including `GET, POST, PATCH, PUT, DELETE`. ### Environment - Cluster/namespace: `pal-e-docs` (API) and `pal-e-production` (frontend) - API ingress: `pal-e-docs.tail5b443a.ts.net` (46d old, uvicorn, FastAPI) - Frontend ingress: `pal-e-production.tail5b443a.ts.net` (7d old, nginx serving SvelteKit SPA, last built 2026-03-28) - API repo: `forgejo_admin/pal-e-api` (renamed from `pal-e-docs` on 2026-03-27 per issue #217) - Frontend repo: `pal-e-docs-app` (renamed from `pal-e-app`, checked out locally at `~/pal-e-app`) - Frontend config: `VITE_PAL_E_DOCS_API_URL` defaults to `https://pal-e-docs.tail5b443a.ts.net` ### Acceptance Criteria - [ ] `CORSMiddleware` added to `src/pal_e_docs/main.py` before the router includes. - [ ] `allow_origins` list is configurable via an env var (e.g. `PAL_E_DOCS_CORS_ORIGINS`, comma-separated) so non-prod deployments can add origins without a code change. - [ ] Default origin list (for prod) includes `https://pal-e-production.tail5b443a.ts.net` and `http://localhost:5173`. - [ ] `allow_methods` includes `GET, POST, PATCH, PUT, DELETE, OPTIONS`. `allow_headers="*"`. `allow_credentials=True` only if the frontend sends credentialed requests (confirm against `src/lib/api-client.ts` which attaches the Keycloak Bearer token). - [ ] A GET with `Origin: https://pal-e-production.tail5b443a.ts.net` returns `access-control-allow-origin: https://pal-e-production.tail5b443a.ts.net` in the response headers. - [ ] An `OPTIONS` preflight with `Origin: https://pal-e-production.tail5b443a.ts.net` and `Access-Control-Request-Method: GET` returns `200`/`204` with `access-control-allow-methods` including `GET`. - [ ] New test in `tests/` asserting CORS headers on a sample endpoint when `Origin` is set. - [ ] Visiting https://pal-e-production.tail5b443a.ts.net/notes/arch-westside-emails in a browser renders the note body without devtools errors. - [ ] Deployed via the existing CI pipeline (Woodpecker on `forgejo_admin/pal-e-api`) — no Tofu/kustomize change needed. - [ ] No regression in curl/SDK access from non-browser callers (they don't care about CORS headers, but confirm the response body shape is unchanged). ### Related - `arch-westside-emails` — the note that surfaced the bug when it couldn't be rendered - `pal-e-docs` — pal-e-docs project, owns `board-pal-e-docs` where this ticket lives - `forgejo_admin/pal-e-api #217` — the rename from `pal-e-docs` to `pal-e-api` - `forgejo_admin/pal-e-platform#278` — **downstream dependency**: hostname swap structural ticket. When the swap lands, the new frontend hostname `pal-e-docs.tail5b443a.ts.net` must be added to the `PAL_E_DOCS_CORS_ORIGINS` env var. The env-var-driven design in this ticket makes that a config update, not a code change. - `src/pal_e_docs/main.py:49` — the line where `CORSMiddleware` must be added - `~/pal-e-app/src/lib/api-client.ts:16` — frontend origin of failing fetches - `arch:notes-api` traceability gap — no backing arch note exists for the API-routes layer; follow-up ticket TBD (accepted as known debt for this ticket)
Author
Contributor

Scope Review: NEEDS_REFINEMENT

Review note: review-971-2026-04-11

Scope is sound. File target, line reference, git-log claim, and tests/ infra all verified. Fits in a single agent pass (<5 min). Two minor traceability gaps noted:

  • [BODY] Add #972 / forgejo_admin/pal-e-platform#278 (hostname swap) to Related — this ticket is a blocker for that design decision.
  • [SCOPE] No arch-notes-api backing note exists. arch-domain-pal-e-docs only documents the entity/table layer, not the FastAPI router layer. Non-blocking for fix — file as a separate backlog item.

Neither recommendation blocks dev dispatch. Scope and fix plan are ready.

## Scope Review: NEEDS_REFINEMENT Review note: `review-971-2026-04-11` Scope is sound. File target, line reference, git-log claim, and tests/ infra all verified. Fits in a single agent pass (<5 min). Two minor traceability gaps noted: - `[BODY]` Add #972 / `forgejo_admin/pal-e-platform#278` (hostname swap) to Related — this ticket is a blocker for that design decision. - `[SCOPE]` No `arch-notes-api` backing note exists. `arch-domain-pal-e-docs` only documents the entity/table layer, not the FastAPI router layer. Non-blocking for fix — file as a separate backlog item. Neither recommendation blocks dev dispatch. Scope and fix plan are ready.
Commenting is not possible because the repository is archived.
No milestone
No project
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/pal-e-api#256
No description provided.