wiki: auto-compile 2026-05-06 (1 log(s), 157 articles)

This commit is contained in:
Vadym Samoilenko 2026-05-06 21:05:03 +01:00
parent 95f2c8b03f
commit 79e1c6dcae
13 changed files with 606 additions and 6 deletions

View file

@ -4,7 +4,7 @@
"syncFolder": "Hoarder",
"attachmentsFolder": "Hoarder/attachments",
"syncIntervalMinutes": 60,
"lastSyncTimestamp": 1778093404068,
"lastSyncTimestamp": 1778097003807,
"updateExistingFiles": false,
"excludeArchived": true,
"onlyFavorites": false,

View file

@ -119,3 +119,6 @@ tags: [daily]
- 20:37 | `cc-dashboard`
- **Asked:** Developer requested calendar view with project time tracking, Azure DevOps sync, and daily task planner for work planning hub.
- **Done:** Implemented calendar display, cache control fixes, and Live Feed bug fixes; deployed to production server successfully.
- 21:05 (<1min) | `memory-compiler`
- **Asked:** Create a knowledge compiler system to extract wiki articles from daily conversation logs.
- **Done:** Compiled 2026-05-06 session into 5 new articles, updated 2 existing articles, and synchronized wiki metadata.

View file

@ -22,8 +22,8 @@ This 3-hop pattern works for hundreds of articles without vector search.
| [[wiki/projects-overview/_index\|projects-overview/]] | All 42 Oliver Agency projects — grouped by server (optical-web-1, optical-dev, baic, box-cli) | 1 |
| [[wiki/tech-patterns/_index\|tech-patterns/]] | Recurring tech stacks: FastAPI, React/Vite, Next.js, Azure AD, AI, Box, One2Edit, Redis/Celery, cost-tracker | 22 |
| [[wiki/architecture/_index\|architecture/]] | Cross-cutting architectural patterns: Docker Compose, multi-agent AI, GCP timeout, RAG, hotfolder, optical-dev deploy, cost-tracker, new-project checklist, troubleshooting playbooks, ADR log, Cloud Run Jobs | 11 |
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M | 6 |
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 103 |
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M, BAIC | 7 |
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 107 |
| [[wiki/connections/_index\|connections/]] | Cross-cutting insights linking 2+ concepts: FastAPI+Azure AD+Docker trinity, AI→cost-tracker, Apache+Vite basePath, GCP→REST polling, Box+hotfolder, Docker DNS+AdGuard, Celery prefork×faster_whisper memory stacking | 10 |
| [[wiki/qa/_index\|qa/]] | Filed answers to queries (saved with `--file-back`) | 0 |
| [[wiki/homelab/_index\|homelab/]] | Self-hosted infra: Proxmox install, IOMMU/PCI passthrough, hypervisor setup, budget builds, HP Elitedesk G3, Homarr API + Apps + Boards + Certificates + Integrations + Settings + Tasks + AdGuard + Clock + Docker Stats + Docker Integration + Download Client + Firewall + Proxmox Integration + Radarr + Readarr + Sonarr + Bookmarks + Calendar + Icons + App Widget + Weather + GitHub + Nextcloud + qBittorrent + RSS Feed + Speedtest Tracker + System Health Monitoring + System Resources + Services Map + Media Stack | 43 |

View file

@ -20,12 +20,13 @@ Per-client notes for clients with 2+ active projects. Covers tech preferences, c
| Barclays | Mod Comms, Banner Builder | FastAPI, React, Gemini, GCP, Docker | [[wiki/client-knowledge/barclays\|barclays]] |
| Ferrero | AC Booking Tool | Node.js, Box API, CSV/OMG | [[wiki/client-knowledge/ferrero\|ferrero]] |
| 3M | OMG Portal | Node.js, Vanilla JS, One2Edit proxy | [[wiki/client-knowledge/3m\|3m]] |
| BAIC | BAIC Dashboard | React + Vite, FastAPI, Azure AD, rsync deploy | [[wiki/client-knowledge/baic\|baic]] |
## Single-Project Clients
These clients have only one project — context lives in the project note:
- **3M** → [[01 Projects/3m-portal/3M OMG Portal|3M OMG Portal]] — One2Edit proxy
- **Barclays** → [[01 Projects/modcomms/Mod Comms|Mod Comms]] — AI proof review, GCP
- **BAIC** → [[01 Projects/baic_dashboard/BAIC Dashboard|BAIC Dashboard]] — Make.com + Azure AD
- **BAIC** → [[wiki/client-knowledge/baic|baic]] — rsync deploy, Azure AD SSO, FastAPI root_path, SSE auth
- **PIMCO** → [[01 Projects/pimco-charts/PIMCO Chart Generator|PIMCO Charts]] — SVG chart generation
- **Ferrero** → [[01 Projects/ferrero-ac-creator/Ferrero AC Booking|Ferrero AC Booking]] — Box API + CSV
- **Solventum** → [[01 Projects/solventum-image-metadata/Solventum Image Metadata|Solventum Metadata]] — OpenAI + enterprise

View file

@ -0,0 +1,56 @@
---
title: "BAIC — Client Knowledge"
aliases: [baic-dashboard, baic-deploy]
tags: [client-knowledge, baic, deploy, rsync, azure-ad]
created: 2026-05-06
updated: 2026-05-06
---
# BAIC
BAIC is a Chinese automotive brand. Oliver Agency maintains the BAIC Dashboard — an internal analytics/reporting tool hosted on the main Oliver web server (`baic` SSH alias).
## Key Facts
- **SSH alias**: `baic``vadym.samoilenko@10.220.72.13` (same server as many Oliver projects — see [[wiki/infrastructure/server-baic]])
- **URL**: `https://baic.oliver.solutions/dashboard/`
- **Auth**: Azure AD / Microsoft SSO via MSAL
- **Stack**: React + Vite (frontend) + FastAPI (backend)
## Deploy Process
> [!important] No git on server — deploy via rsync only
```bash
# 1. Build locally
npm run build
# 2. Sync dist/ to server (no git pull on server)
rsync -avz --delete dist/ baic:/var/vhosts/baic.oliver.solutions/htdocs/dashboard/
```
- Remote path: `/var/vhosts/baic.oliver.solutions/htdocs/dashboard/`
- Backend systemd service: `baic_dashboard.service` (underscore, not hyphen)
```bash
# Restart backend after Python changes
ssh baic "sudo systemctl restart baic_dashboard.service"
# Check status
ssh baic "sudo systemctl status baic_dashboard.service"
```
## Known Gotchas
| Gotcha | Detail | Article |
|--------|--------|---------|
| FastAPI `root_path` route stripping | Routes must be registered WITHOUT `/dashboard` prefix | [[wiki/concepts/fastapi-root-path-route-stripping]] |
| SSE needs `?token=` | `EventSource` can't set custom headers | [[wiki/concepts/sse-jwt-query-param]] |
| SPA index.html must be no-cache | Old hashes cause blank screen after rebuild | [[wiki/concepts/spa-index-html-cache-control]] |
| MS SSO IDs are not UUIDs | `ms-4n0T2x-...` format breaks UUID validators | [[wiki/concepts/microsoft-sso-non-uuid-ids]] |
## Related
- [[wiki/infrastructure/server-baic]] — server overview (hosts 40+ domains, not just BAIC)
- [[wiki/tech-patterns/azure-ad-msal-auth]] — MSAL SSO pattern
- [[wiki/tech-patterns/react-vite-typescript]] — frontend stack

View file

@ -120,5 +120,10 @@
| [[wiki/concepts/icloud-space-2-duplicate-files\|iCloud Concurrent Write: ' 2.md' Duplicate Files]] | When two devices write to the same iCloud Drive file at the same time, iCloud resolves the conflict | memory-compiler | 2026-05-05 |
| [[wiki/concepts/one2edit-username-format\|One2Edit Username Format]] | One2Edit usernames are **email addresses**, not `firstname.lastname` handles. This trips up anyone g | 01 Projects/3m-portal | 2026-05-05 |
| [[wiki/concepts/sqlite-not-null-as-boolean\|SQLite: IS NOT NULL AS Boolean]] | When a column contains sensitive data (e.g. a password hash), never select it directly to answer a y | sandbox | 2026-05-05 |
| [[wiki/concepts/fastapi-root-path-route-stripping]] | Starlette strips `root_path` prefix before route matching — register `/healthz` not `/cc-dashboard/healthz` even when app is at that prefix | daily/2026-05-06.md | 2026-05-06 |
| [[wiki/concepts/sse-jwt-query-param]] | Browser `EventSource` has no headers option — JWT must travel as `?token=` query param for SSE streams | daily/2026-05-06.md | 2026-05-06 |
| [[wiki/concepts/spa-index-html-cache-control]] | Vite asset hashes change on rebuild — old cached `index.html` causes 404 on JS chunks; fix: `no-cache` on index.html, `immutable` on `/assets/` | daily/2026-05-06.md | 2026-05-06 |
| [[wiki/concepts/microsoft-sso-non-uuid-ids]] | Microsoft SSO app IDs are not UUIDs (`ms-4n0T2x-...`) — UUID validation in routing/DB silently fails for SSO users | daily/2026-05-06.md | 2026-05-06 |
<!-- Articles added automatically by compile.py -->
<!-- Format: | [[concepts/slug]] | One-line summary | daily/YYYY-MM-DD.md | date | -->

View file

@ -5,7 +5,7 @@ tags: [apache, proxy, mod_alias, gotcha, optical-dev, deployment]
sources:
- "daily/2026-04-30.md"
created: 2026-04-30
updated: 2026-04-30
updated: 2026-05-06
---
# Apache mod_alias Takes Priority Over mod_proxy
@ -70,6 +70,30 @@ If ProxyPass rules appear correct but API calls return static HTML (or 404s from
2. Verify the physical directory exists: `ls /var/www/html/<path>`
3. If both exist, you have this conflict
### Fix 3: ProxyPassMatch with Explicit Prefix Stripping
When using a FastAPI app with `root_path` behind a conflicting Alias, `ProxyPassMatch` can strip the prefix manually:
```apache
# Static files served by Alias
Alias /cc-dashboard /var/www/html/cc-dashboard
# API proxy: capture everything AFTER /cc-dashboard/ and forward without prefix
# FastAPI receives /api/... not /cc-dashboard/api/...
ProxyPassMatch ^/cc-dashboard/api/(.*)$ http://127.0.0.1:8001/api/$1
ProxyPassMatch ^/cc-dashboard/healthz$ http://127.0.0.1:8001/healthz
# RewriteRule [P,L] alternative (same effect)
RewriteEngine On
RewriteRule ^/cc-dashboard/api/(.*)$ http://127.0.0.1:8001/api/$1 [P,L]
```
This pattern is required when:
1. An `Alias` covers the same prefix (so simple `ProxyPass` never fires)
2. The FastAPI app is configured with `root_path="/cc-dashboard"` (routes registered without prefix)
See [[wiki/concepts/fastapi-root-path-route-stripping]] for why FastAPI routes must NOT include the prefix.
## Related Concepts
- [[wiki/concepts/apache-proxypass-include-files-ignored]] — related Apache silent failure: ProxyPass in Include fragments ignored

View file

@ -0,0 +1,90 @@
---
title: "FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching"
aliases: [fastapi-root-path, starlette-root-path-routes, fastapi-behind-prefix]
tags: [fastapi, starlette, routing, deployment, gotcha, apache, reverse-proxy]
sources:
- "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---
# FastAPI root_path Does Not Add a Prefix — Starlette Strips It Before Route Matching
When FastAPI is initialised with `root_path="/cc-dashboard"`, Starlette **strips** that prefix from the incoming URL before matching routes. Routes must therefore be registered **without** the prefix. Registering `/cc-dashboard/healthz` results in a permanent 404 — the router never sees the prefix in the path.
## Key Takeaways
- `root_path` is metadata for OpenAPI docs and proxy headers only — it is NOT prepended to route paths
- Starlette removes `root_path` from the URL before route matching; routes see the stripped path
- Register `/healthz`, not `/cc-dashboard/healthz`, even when the app is served at `/cc-dashboard/`
- The Apache `ProxyPass /cc-dashboard/ http://127.0.0.1:8001/` strips the prefix before forwarding — FastAPI's `root_path` merely tells the app "I am mounted at this prefix" for docs generation
- This behaviour is **not documented** on the FastAPI site; it is a Starlette ASGI mount convention
## Details
### The Counterintuitive Initialisation
```python
# app is served by Apache at /cc-dashboard/
app = FastAPI(root_path="/cc-dashboard")
# ✅ CORRECT — Starlette strips root_path, route matches /healthz
@app.get("/healthz")
async def healthz():
return {"ok": True}
# ❌ WRONG — route registered as /cc-dashboard/healthz
# but Starlette sees /healthz after stripping, so this never matches
@app.get("/cc-dashboard/healthz")
async def healthz_wrong():
return {"ok": True}
```
### How the Request Flows
```
Browser → GET /cc-dashboard/healthz
Apache ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
↓ (Apache strips /cc-dashboard prefix)
Starlette receives → GET /healthz (root_path="/cc-dashboard" stored separately)
Route matching: /healthz ← this is what must be registered
```
### What root_path Actually Does
```python
# root_path is used by Starlette for:
# 1. OpenAPI/Swagger docs — generates correct absolute URLs in the schema
# 2. url_for() — builds URLs with the prefix included
# 3. Request.root_path — accessible in middleware for logging/tracing
app = FastAPI(root_path="/cc-dashboard")
# Swagger UI served at /cc-dashboard/docs will use /cc-dashboard/openapi.json
# url_for("healthz") → "/cc-dashboard/healthz"
```
### Corresponding Apache Config
```apache
# Apache strips /cc-dashboard before proxying
ProxyPass /cc-dashboard/ http://127.0.0.1:8001/
ProxyPassReverse /cc-dashboard/ http://127.0.0.1:8001/
# Trailing slash on BOTH sides is mandatory —
# without it Apache passes the prefix intact → all routes 404
```
> [!warning] ProxyPassMatch stripping variant
> If using `ProxyPassMatch ^/cc-dashboard/(.*)$ http://127.0.0.1:8001/$1`, the `$1` capture group performs the same stripping manually. This is needed when you cannot use the simple `ProxyPass /prefix/ http://host/` form (e.g. when an `Alias` conflict exists — see [[wiki/concepts/apache-mod-alias-proxy-priority]]).
## Related
- [[wiki/concepts/apache-mod-alias-proxy-priority]] — Alias/ProxyPass conflict that forces use of ProxyPassMatch
- [[wiki/concepts/nextjs-basepath-auth-redirects]] — similar prefix-stripping gotcha in Next.js basePath
- [[wiki/tech-patterns/fastapi-python-docker]] — standard Oliver FastAPI deployment pattern
## Sources
- [[daily/2026-05-06.md]] — BAIC dashboard deployment: routes registering with full prefix caused 404s

View file

@ -0,0 +1,126 @@
---
title: "Microsoft SSO User IDs Are Not UUIDs — Azure Object IDs vs Custom ID Formats"
aliases: [ms-sso-ids, azure-ad-non-uuid, microsoft-user-id-format]
tags: [azure-ad, msal, sso, uuid, gotcha, auth, mongodb, fastapi]
sources:
- "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---
# Microsoft SSO User IDs Are Not UUIDs — Azure Object IDs vs Custom ID Formats
Microsoft SSO user identifiers from MSAL/Azure AD do not follow the UUID format. IDs can look like `ms-4n0T2x-aW4w1pO1OKYNf` — a custom alphanumeric format. Any code that validates user IDs as UUIDs (regex, Pydantic `UUID` type, MongoDB ObjectId coercion) will silently fail for SSO users, blocking login, profile creation, or data access.
## Key Takeaways
- Azure AD `objectId` IS a proper UUID (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) — but it may be transformed by the application into a custom format
- App-level IDs derived from SSO tokens (e.g. with a `ms-` prefix) are **not** UUIDs
- Any `re.match(UUID_PATTERN, user_id)` check, Pydantic `field: UUID`, or `ObjectId(user_id)` will raise/return None for non-UUID IDs
- Use `str` for user ID fields — never coerce to UUID/ObjectId unless you fully control ID generation
- The failure mode is silent: ID validation returns `None`, user is treated as "not found", and downstream code (email, notifications, access checks) never runs
## Details
### The ID Format Mismatch
```python
# Standard UUID pattern used in many codebases
UUID_PATTERN = re.compile(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
re.IGNORECASE
)
# Microsoft Azure AD objectId (direct) — IS a UUID ✓
azure_object_id = "a3d8e2f1-4b5c-6d7e-8f9a-0b1c2d3e4f5a"
# App-transformed SSO ID — NOT a UUID ✗
app_sso_id = "ms-4n0T2x-aW4w1pO1OKYNf"
# Validation fails silently
if not UUID_PATTERN.match(app_sso_id):
return None # SSO user is treated as "not found"
```
### Where This Breaks
**FastAPI path parameters with UUID type:**
```python
# ❌ FastAPI raises 422 Unprocessable Entity for non-UUID IDs
@app.get("/users/{user_id}")
async def get_user(user_id: UUID): # rejects "ms-4n0T2x-..."
...
# ✅ Use str, validate business rules separately
@app.get("/users/{user_id}")
async def get_user(user_id: str):
...
```
**Pydantic model UUID fields:**
```python
from pydantic import BaseModel
from uuid import UUID
# ❌ Pydantic v2 raises ValidationError for non-UUID string
class UserRef(BaseModel):
user_id: UUID # breaks for SSO users
# ✅ Keep as str
class UserRef(BaseModel):
user_id: str
```
**MongoDB ObjectId coercion:**
```python
from bson import ObjectId
# ❌ ObjectId() raises InvalidId for non-24-char-hex strings
user = await db.users.find_one({"_id": ObjectId(user_id)})
# ✅ Store user_id as a plain string field, not as _id ObjectId
user = await db.users.find_one({"user_id": user_id})
```
### Azure AD objectId vs App-Level IDs
| Source | Format | Example | Is UUID? |
|--------|--------|---------|----------|
| Azure AD `objectId` (raw) | UUID v4 | `a3d8e2f1-4b5c-...` | ✓ Yes |
| MSAL `homeAccountId` | `<oid>.<tid>` | `a3d8e2f1...tenant123` | ✗ Partial |
| App-transformed ID | Custom | `ms-4n0T2x-aW4w1pO1OKYNf` | ✗ No |
| Local user IDs | UUID or ObjectId | Depends on stack | Controlled |
### Recommendation
```python
# Store SSO users with a clear prefix and treat user_id as opaque string
async def get_or_create_sso_user(token_claims: dict) -> dict:
azure_oid = token_claims["oid"] # Azure objectId — UUID format
# Either use azure_oid directly (valid UUID), or apply a prefix
# but be CONSISTENT across the codebase
user_id = azure_oid # ✅ use raw Azure objectId — it IS a UUID
user = await db.users.find_one({"user_id": user_id})
if not user:
await db.users.insert_one({
"user_id": user_id, # stored as str
"email": token_claims["preferred_username"],
"source": "azure_sso",
})
return user
```
> [!warning] The silent failure pattern
> When ID validation fails in middleware or a helper function and returns `None`, the caller often proceeds with `None` as a user reference. This causes `find_one({"user_id": None})` — which may return an unrelated document or None — rather than raising an auth error. Always fail loudly on invalid IDs.
## Related
- [[wiki/concepts/azure-ad-yaml-allowlist-pattern]] — Azure AD auth + YAML authZ pattern used across Oliver projects
- [[wiki/concepts/msal-vanilla-js-pkce]] — MSAL.js PKCE flow that produces the token claims
- [[wiki/tech-patterns/azure-ad-msal-auth]] — Oliver standard SSO pattern
## Sources
- [[daily/2026-05-06.md]] — BAIC dashboard: SSO users with `ms-` prefixed IDs failing UUID validation in route handlers

View file

@ -5,7 +5,7 @@ tags: [security, middleware, regex, xss, fastapi, python]
sources:
- "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
updated: 2026-05-06
---
# Security Middleware Regex Matching JSON Keys Causes False Positives
@ -101,6 +101,57 @@ def is_suspicious_value(value: str) -> bool:
| `evaluate_result` | `eval` |
| `expression_type` | `expression` |
## Command Injection Patterns: `\b` Word Boundary Is Mandatory
XSS patterns are not the only problem. **Command injection** regex patterns suffer the same false-positive issue when applied to raw JSON text, and the fix is different: word boundaries (`\b`) are required because HTML-context markers don't apply.
### The False Positive Table for Command Tokens
| Pattern (no `\b`) | False positive example |
|-------------------|----------------------|
| `sh\s+` | `"Josh Smith"` — the `sh ` in "Josh S..." matches |
| `rm\s+` | `"Norm "``rm ` in "Norm " matches |
| `nc\s+` | Any name containing `nc` followed by a space |
| `wget\s+` | Field value `"get widgets"``get w` doesn't match, but `wget` as substring would |
| `curl\s+` | `"security curl"` — substring match |
### Fix: Prefix All Command Tokens with `\b`
```python
# ❌ BAD — matches substrings in names, field values, natural text
COMMAND_INJECTION = re.compile(
r'(sh\s+|rm\s+|nc\s+|wget\s+|curl\s+|bash\s+|python\s+|perl\s+)',
re.IGNORECASE
)
# ✅ GOOD — \b ensures we only match at a word boundary
COMMAND_INJECTION = re.compile(
r'(\bsh\b|\brm\b|\bnc\b|\bwget\b|\bcurl\b|\bbash\b|\bpython\b|\bperl\b)',
re.IGNORECASE
)
# Note: \b before the token, but also after — \bsh\b won't match "Josh" or "shell"
```
### Combined Pattern (XSS + Command Injection, Value-Only Scan)
```python
# Apply after parsing JSON — scan VALUES only
XSS_PATTERNS = re.compile(
r'(<\s*script|javascript\s*:|on\w+\s*=)',
re.IGNORECASE
)
CMD_INJECTION = re.compile(
r'(\bsh\b|\brm\s+-rf|\bnc\b\s+\S+\s+\d+|\bwget\b|\bcurl\b)',
re.IGNORECASE
)
def is_suspicious_value(value: str) -> bool:
return bool(XSS_PATTERNS.search(value) or CMD_INJECTION.search(value))
```
> [!tip] Parse first, then scan
> Word boundaries reduce false positives but don't eliminate them for common 2-letter tokens like `rm` and `nc`. The most robust approach is always: **parse JSON first, scan string values only** — as described in Fix 1 above.
## Related Concepts
- [[wiki/tech-patterns/xss-validation-false-positive-word-boundary]] — existing tech-pattern covering this same fix

View file

@ -0,0 +1,110 @@
---
title: "SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild"
aliases: [spa-cache-control, vite-cache-broken-spa, index-html-no-cache]
tags: [vite, spa, caching, apache, deployment, gotcha, react]
sources:
- "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---
# SPA index.html Must Have no-cache Headers — Vite Asset Hash Mismatch After Rebuild
After a Vite rebuild, JavaScript chunk filenames change (content-hash in filename). If the browser has cached the old `index.html`, it will request the old hash filenames — which no longer exist — and the SPA fails to load with 404 errors on JS/CSS chunks. The fix is to instruct browsers and CDNs to never cache `index.html` while allowing long-lived caching for the hashed asset files.
## Key Takeaways
- Vite generates filenames like `main-BfD92kXz.js` — hash changes on every rebuild
- Old `index.html` cached in browser references old hash → 404 on JS chunks → blank page or partial load
- `index.html` must always be served with `Cache-Control: no-cache, no-store, must-revalidate`
- Hashed assets (`*.js`, `*.css` in `/assets/`) can be cached indefinitely (`max-age=31536000, immutable`)
- The symptom is a white screen or console errors like `Failed to load module script` after deploying
## Details
### Apache: Two-Rule Cache Pattern
```apache
<VirtualHost *:443>
DocumentRoot /var/www/html/my-spa
# index.html — never cache (Vite rewrites it on every build)
<Files "index.html">
Header always set Cache-Control "no-cache, no-store, must-revalidate"
Header always set Pragma "no-cache"
Header always set Expires "0"
</Files>
# Hashed assets — cache forever (hash in filename guarantees uniqueness)
<LocationMatch "^/assets/.*\.(js|css|woff2?|png|svg|ico)$">
Header always set Cache-Control "public, max-age=31536000, immutable"
</LocationMatch>
# SPA fallback — all routes serve index.html
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ /index.html [L]
</VirtualHost>
```
### Nginx Equivalent
```nginx
server {
root /var/www/html/my-spa;
# Never cache index.html
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Cache hashed Vite assets forever
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}
```
### Diagnosing the Problem
When users report "blank screen after deployment" or "old version still showing":
1. Open DevTools → Network → filter by `index.html`
2. Check Response Headers for `Cache-Control`
3. If `max-age > 0` or `Cache-Control` is absent — this is the bug
4. Hard refresh (`Ctrl+Shift+R`) will load the new version, confirming cache as root cause
5. Check console for `Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html"` — this is the 404-as-HTML fallback
### Why Hashed Assets Can Have Infinite Cache
Vite's build output:
```
dist/
index.html ← changes every build (references new hashes)
assets/
main-BfD92kXz.js ← hash in filename; content is immutable for this URL
vendor-CqRt4mPn.js
index-Dx8kLpWw.css
```
Because the filename itself is the cache key and contains the content hash, old URLs are simply abandoned and new hashes take their place. There is no risk of stale content for hashed files.
> [!note] Same issue affects sub-path deployments
> When serving a SPA at a prefix (e.g. `/dashboard/`), the `<Files "index.html">` directive must cover the actual served path. If using [[wiki/concepts/vite-prebuilt-subpath-workaround]] with `<base href>`, verify the same no-cache rule covers the entry point.
## Related
- [[wiki/concepts/vite-prebuilt-subpath-workaround]] — Vite `<base href>` pattern for sub-path deployments
- [[wiki/concepts/shell-static-deploy-patterns]] — static deploy scripts: cp, rsync, Apache reload
- [[wiki/tech-patterns/react-vite-typescript]] — standard Oliver SPA stack
## Sources
- [[daily/2026-05-06.md]] — BAIC dashboard: blank screen after Vite rebuild; old index.html referencing non-existent chunk hashes

View file

@ -0,0 +1,128 @@
---
title: "SSE / EventSource Does Not Support Custom Headers — Pass JWT as Query Param"
aliases: [sse-no-headers, eventsource-jwt, sse-auth-token]
tags: [sse, eventsource, jwt, auth, browser, security, gotcha]
sources:
- "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---
# SSE / EventSource Does Not Support Custom Headers — Pass JWT as Query Param
The browser-native `EventSource` API cannot set custom HTTP headers. There is no `headers` option in the constructor. For JWT-authenticated SSE streams, the token must be appended as a query parameter (`?token=...`). This is a known browser limitation with no workaround short of switching to `fetch()` with a `ReadableStream`.
## Key Takeaways
- `new EventSource(url)` sends only cookies and browser-default headers — no way to add `Authorization: Bearer`
- JWT must travel as a URL query parameter: `?token=<jwt>` for browser-native SSE
- Query-param tokens are visible in server access logs — use short-lived tokens (< 60 s) and rotate them
- Alternative: use `fetch()` + `ReadableStream` (SSE-over-fetch) which does support custom headers, but requires manual reconnect logic
- Backend must accept both `Authorization` header (for regular API calls) and `?token=` (for SSE)
## Details
### The Problem
```javascript
// ❌ This is the intuitive approach — but EventSource has no headers option
const es = new EventSource("/stream", {
headers: { Authorization: `Bearer ${token}` } // IGNORED — not a valid option
});
```
### Fix: Token as Query Parameter
```javascript
// ✅ Pass token as query param
const token = localStorage.getItem("access_token");
const es = new EventSource(`/stream?token=${encodeURIComponent(token)}`);
es.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(data);
};
es.onerror = (err) => {
es.close();
// Reconnect logic here
};
```
### FastAPI Backend: Accept Both Header and Query Param
```python
from fastapi import Query, Header, HTTPException
from typing import Optional
async def get_current_user_sse(
token: Optional[str] = Query(None),
authorization: Optional[str] = Header(None),
):
"""Auth handler that accepts JWT from header OR query param (for SSE)."""
raw_token = None
if authorization and authorization.startswith("Bearer "):
raw_token = authorization.removeprefix("Bearer ")
elif token:
raw_token = token
else:
raise HTTPException(status_code=401, detail="Missing token")
return verify_jwt(raw_token) # your existing JWT verifier
@app.get("/stream")
async def stream_events(
request: Request,
user = Depends(get_current_user_sse),
):
async def event_generator():
while True:
if await request.is_disconnected():
break
yield {"data": json.dumps({"status": "ok"})}
await asyncio.sleep(2)
return EventSourceResponse(event_generator())
```
### Alternative: fetch() + ReadableStream (Supports Headers)
```javascript
// ✅ Full header support, but you must implement reconnect manually
const response = await fetch("/stream", {
headers: { Authorization: `Bearer ${token}` },
signal: abortController.signal,
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE text format manually: "data: {...}\n\n"
processSSEChunk(text);
}
```
### Security Considerations
| Approach | Token exposure | Reconnect | Browser support |
|----------|---------------|-----------|-----------------|
| `?token=` query param | Server logs, referrer headers | Automatic | Universal |
| `fetch()` + ReadableStream | Headers only (safe) | Manual | Modern browsers |
| Cookie (httpOnly) | Minimal | Automatic | Universal, requires CORS cookie config |
> [!tip] Prefer short-lived tokens for SSE
> If using query param auth, generate a dedicated short-lived SSE token (60300 s) via a separate endpoint. This limits the window of exposure if a token is leaked via logs.
## Related
- [[wiki/concepts/websocket-react-token-guard]] — WebSocket token guard pattern (similar auth-before-connect problem)
- [[wiki/concepts/double-submit-cookie-csrf]] — alternative cookie-based stateless auth that avoids the query-param exposure
- [[wiki/concepts/zustand-async-hydration]] — Zustand hydration delay that can cause token=null on first SSE connect
## Sources
- [[daily/2026-05-06.md]] — BAIC dashboard live feed: EventSource rejecting Authorization header

View file

@ -1,6 +1,12 @@
# Build Log
## [2026-05-06T21:00:00+01:00] compile | 2026-05-06.md
- Source: daily/2026-05-06.md
- Articles created: [[wiki/concepts/fastapi-root-path-route-stripping]], [[wiki/concepts/sse-jwt-query-param]], [[wiki/concepts/spa-index-html-cache-control]], [[wiki/concepts/microsoft-sso-non-uuid-ids]], [[wiki/client-knowledge/baic]]
- Articles updated: [[wiki/concepts/security-middleware-json-key-regex]] (added command injection `\b` word boundary section with false-positive table for `sh`, `rm`, `nc`); [[wiki/concepts/apache-mod-alias-proxy-priority]] (added Fix 3: ProxyPassMatch stripping pattern for FastAPI-behind-prefix)
- Index updates: [[wiki/concepts/_index]] (123→127); [[wiki/client-knowledge/_index]] (6→7, BAIC added to table); [[wiki/_master-index]] (concepts 103→127, client-knowledge 6→7)
## [2026-05-05T21:00:00+01:00] compile | 2026-05-05.md (pass 2)
- Source: daily/2026-05-05.md
- Articles created: [[wiki/concepts/icloud-space-2-duplicate-files]], [[wiki/concepts/one2edit-username-format]], [[wiki/concepts/sqlite-not-null-as-boolean]]