CORS and origins
Worker pickOrigin: local-dev, product hosts, project-scoped Pages previews, ALLOWED_ORIGIN list, Flask constraints.
This page
CORS and origins
Abstract
AXIS is a browser product that talks to cross-origin backends (Flask, Worker, venues). CORS misconfiguration is the #1 local-deploy footgun after missing wheels.
The Worker implements an explicit origin picker (pickOrigin in worker/src/index.ts); Flask and venue APIs have their own rules.
Conceptual model
Rendering…
Worker behavior
Resolution order for Access-Control-Allow-Origin:
1. Local-dev (always echoed)
Any http:// or https:// origin on localhost or 127.0.0.1, optional port
(e.g. Vite :3000, axis_pwa_server :8081, Playwright ephemeral ports).
Not allowed (and not needed): http://0.0.0.0:… — browsers do not use 0.0.0.0 as a page origin; binding with HOST=0.0.0.0 only means “listen on all interfaces.”
Localhost/127 allowlisting is safe for CORS: only pages actually served from those hosts present that Origin. A public site cannot forge Origin: http://localhost:3000 in a real browser.
2. Product hosts (2.0.1+)
Exact product / HOOX / legacy PYNE hosts under:
*.hoox.sh/hoox.sh*.pynescript.online/pynescript.online
Examples: https://axis.hoox.sh (canonical Pages), https://hoox.sh, https://pynescript.online.
3. Project-scoped Cloudflare® Pages (not open *.pages.dev)
Canonical Pages is https://axis.hoox.sh (product host above). Preview hashes on *.axis.pages.dev (and the apex axis.pages.dev) are also echoed. Legacy *.pynescript-axis.pages.dev still matches. Arbitrary third-party Pages projects (evil.pages.dev, other apps) fall through to the allowlist fallback — they are not reflected.
4. ALLOWED_ORIGIN allowlist
Comma-separated exact origins in env.ALLOWED_ORIGIN (e.g. https://a.example,https://b.example). If the request Origin is listed, echo it; otherwise return the first entry (default https://pynescript.online).
CORS headers applied:
Access-Control-Allow-Origin: <picked>
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-Admin-Token, If-Match
Access-Control-Max-Age: 86400
Vary: Origin
OPTIONS → 204 preflight.
Production implication
For a custom domain already under product regex, no extra var is required. For one-off preview hosts outside the project, list them:
ALLOWED_ORIGIN = "https://my-preview.example.com,https://axis.example.com"
Flask / server engine
The server engine posts from the browser to the configured endpoint. Flask Pro API must:
- Answer
OPTIONSpreflight if cross-origin. - Reflect or allow the PWA origin.
- Accept
Content-Type: application/json.
Pyne always appends the same product Origin regex as Worker pickOrigin (including
*.axis.pages.dev) even when systemd ALLOWED_ORIGINS is a short list.
GET /health (AXIS Settings probe) and POST /run are free CORS paths — they
echo any Origin so a Pages preview can reach https://axis.hoox.sh without a
per-preview allowlist entry.
Same-origin reverse proxy (VPS demo) eliminates CORS
when the PWA and Pro API share https://axis.hoox.sh. Cross-origin Pages → VPS
still needs those headers (they are now default).
Venue sources/streams
Public Binance/OKX/etc. REST must send CORS headers usable by browsers. Firefox often reports “CORS request did not succeed / status (null)” when the TCP/TLS connection fails entirely (geo block, firewall, extension) — that is not a missing Access-Control-Allow-Origin header.
AXIS resilience for Binance:
- Direct hosts:
api.binance.comthendata-api.binance.vision(src/data/binance-http.ts). - Worker allowlisted proxy:
GET /api/market/binance/klines|ticker/24hr|exchangeInfoon the AXIS Worker (worker/src/market.ts). - Live WS rotates
:9443→:443→data-stream.binance.vision(port 9443 is frequently firewalled). - Offline last resort:
mock-walk/mock-poll, or warm bars-cache.
Prefer the DO relay (cf-do stream / /api/stream) when many tabs share one upstream WS — still requires the Worker edge to reach Binance.
AXIS resilience for MEXC:
- Worker allowlisted proxy first:
GET /api/market/mexc/{klines,ticker/24hr,exchangeInfo}(src/data/mexc-http.ts). Publicapi.mexc.comdoes not sendAccess-Control-Allow-Origin. - Direct
api.mexc.comonly as last-ditch fallback (offline lab, tests, or Worker 5xx). Worker 4xx is not retried against the CORS-blocked host.
Dynamic plugins
- Module import of plugin JS: needs CORS on the script origin. Prefer same-origin
/plugins/*(PYNE Agent ships as/plugins/axis-pine-agent.js). - Remote modules (e.g. legacy
pyne-agent-worker…/plugin/axis-pine-agent.js) need ACAO + CSPscript-srcallow when a CSP is present. - fetch inside plugin: subject to third-party CORS independently (agent API still uses the pyne-agent Worker origin).
Invariants & edge cases
- Credentials — Worker CORS does not set
Allow-Credentials: true; use Bearer headers, not cookies. - Multiple prod domains — product regex + comma-separated
ALLOWED_ORIGINcover multi-host; unknown sites never get open reflection. - Vite vs 8081 — both covered for Worker; Flask config must match whichever you use.
- Health checks from curl omit Origin — still work; browser calls need correct ACAO.
- No open
*.pages.dev— only the Pages projectaxispreviews (*.axis.pages.dev) are product-scoped.
Failure modes
| Browser console | Meaning |
|---|---|
| blocked by CORS policy | Origin not allowlisted |
| preflight 404 | Server lacks OPTIONS |
| No ACAO on error body | Some error paths forgot headers (Worker try/catch mostly covered) |