AI Frontier

OpenClaw gateway CORS and OPTIONS preflight on macOS in 2026: allowed origins, credentials, nginx edge headers, and cloud Mac mini rehearsal

MacHTML Lab2026.05.1533 min read

When product teams wire a browser dashboard or internal SPA to an OpenClaw gateway listening on TCP 8787 in 2026, the first “mystery outage” is rarely the model API—it is CORS. The gateway answers fine from curl, yet Safari and Chrome show blocked by CORS policy because the browser sent an OPTIONS preflight the edge never handled, or because Access-Control-Allow-Origin was set to * while the fetch used cookies. This runbook walks operators through explicit origin allowlists, credentialed versus simple requests, nginx header placement, and reproducible curl probes you can paste into tickets—aligned with first-run PATH and Node checks, port binding and health-probe alignment, and doctor diagnostics.

You will leave with a decision matrix, header recipes, numeric guardrails (204 or 200 acceptable OPTIONS responses, no wildcard when credentials: 'include'), and a rental baseline near $16.9 per day on published MacHTML pricing so you can rehearse the same Safari build your customers use.

How browsers differ from curl

curl https://gateway.example/health proves reachability but not browser policy. User agents append an Origin header for cross-site calls, may upgrade GETs to preflighted POSTs when custom headers appear, and refuse to expose response bodies to JavaScript unless Access-Control-Allow-Origin matches the initiating origin (or is * on non-credentialed responses). Treat CORS as a contract between two teams: frontend owns the exact origin strings; platform owns the header emission order on the hop that terminates TLS.

Document every caller origin with scheme and port—https://app.corp.internal:8443 is distinct from https://app.corp.internal. Missing the port in your allowlist causes intermittent failures only in staging where TLS terminates on a non-default port. That class of bug is why teams rehearse on a dedicated Mac mini with the same Safari build as executives’ laptops.

Simple versus credentialed fetches

Simple GET requests without custom headers may skip preflight, yet still require Access-Control-Allow-Origin if JavaScript must read the body cross-origin. The moment you add Authorization, Cookie, or fetch(..., { credentials: 'include' }), browsers require a specific echoed origin—wildcards are forbidden by specification. Pair that with Access-Control-Allow-Credentials: true on the gateway or proxy.

If you only expose anonymous metrics, you can keep responses public with * and avoid cookies entirely. Mixed modes—some routes credentialed, others anonymous—split your nginx location blocks so marketing pixels never inherit admin CORS headers by accident.

OPTIONS preflight mechanics

Preflight sends OPTIONS with Access-Control-Request-Method and optional Access-Control-Request-Headers. The gateway must answer with Access-Control-Allow-Methods listing the verbs you permit (usually GET,POST,OPTIONS), Access-Control-Allow-Headers echoing requested header names case-insensitively, and Access-Control-Max-Age if you want browsers to cache the handshake—common values are 300 seconds during development and 600–86400 in production depending on how often you rotate headers.

Return 204 with no body or 200 with an empty body; both are widely accepted. What fails is 405 Method Not Allowed because nginx routed OPTIONS to the wrong upstream, or missing Vary: Origin when you echo dynamic origins—CDNs may otherwise cache the wrong ACAO for every customer behind the same edge.

Origin allowlists and localhost traps

http://localhost:5173 (Vite) and http://127.0.0.1:5173 are different origins. Add both during local development or standardize the dev command to one hostname. For SSH port-forwards that expose 127.0.0.1:8787 on engineers’ laptops, remember the browser origin is still the SPA’s public URL, not the loopback hop—your allowlist must include the SPA origin, not only http://127.0.0.1.

Dynamic reflection—copying any incoming Origin into Access-Control-Allow-Origin—is convenient but dangerous unless you validate against a fixed set server-side. Prefer a map lookup: if origin in set S, echo it; else omit the header and let the browser fail closed. Log rejected origins at INFO with sampling so security can audit abuse attempts without drowning disks.

nginx and double-header pitfalls

When nginx terminates TLS and forwards to OpenClaw on loopback, decide whether CORS lives in nginx or the Node process—doing both yields duplicate headers that some browsers reject. A pragmatic split: nginx handles CORS for static assets and marketing pages; OpenClaw handles API-only routes if the gateway already emits headers for CLI compatibility. Whatever you choose, document it in the same runbook as graceful shutdown so on-call engineers do not “fix” CORS twice during incidents.

If you terminate HTTP/2 at nginx, ensure add_header directives apply to both success and error paths—older nginx configs only attach headers on 2xx unless you set always. OPTIONS requests that hit the wrong server block are the second most common cause of mysterious SPA failures after wildcard + credentials mismatch.

Vary: Origin, CDN caching, and stale ACAO

When you echo the requesting origin, responses become variant on the Origin request header. If you omit Vary: Origin, intermediate caches may serve one customer’s Access-Control-Allow-Origin to another customer’s browser session—a subtle data leak as well as a functional bug. CDNs that normalize headers aggressively are notorious for stripping Vary; open a ticket with your provider if edge rules rewrite CORS.

Set short TTLs on cached error bodies (60 seconds or less) while debugging CORS so a bad deploy does not stick for hours. Pair cache busting with correlation IDs from your structured logging article so you can match HAR files to gateway logs even when users anonymize screenshots.

Custom headers, JWTs, and tool-specific CORS

OpenClaw tool routes often add X-Request-Id or X-OpenClaw-Profile style headers. Any non-safelisted header triggers preflight, so enumerate them in Access-Control-Allow-Headers explicitly—do not rely on * for credentialed flows. If JWTs travel in Authorization: Bearer, confirm both the preflight and the actual POST return consistent ACAO lines; mismatches there produce the infamous “has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header” even when the POST succeeded server-side.

When rotating signing keys, double the preflight cache window temporarily so browsers pick up new header names within 15 minutes; then restore the tighter max-age. Document that knob in the same ops calendar as TLS certificate renewals so rotations stay synchronized.

Decision matrix

CallerCredentialed?ACAO strategyNotes
Public docs siteNo* or static allowlistKeep anonymous; no cookies
Internal admin SPAYesEcho explicit originPair with Allow-Credentials: true
Mobile WebViewSometimesCustom scheme originsValidate WebView URL patterns
Third-party SaaS embedRareStatic partner originContractual allowlist only

curl probes you can archive

# Preflight probe (replace host/path)
curl -i -X OPTIONS "https://gw.example/v1/chat" \
  -H "Origin: https://app.example" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

# Simple GET with Origin to verify ACAO on GET
curl -i "https://gw.example/health" \
  -H "Origin: https://app.example"

Store the full response headers in your ticket; diff them after each deploy. When OPTIONS latency exceeds 150 ms p95 at the edge while GET health stays 12 ms, investigate routing to a cold upstream rather than blaming OpenClaw itself.

Rollout checklist

  1. Enumerate every production and staging SPA origin with scheme, host, and port.
  2. Decide single owner for CORS headers—nginx or OpenClaw—and remove duplicates.
  3. Run OPTIONS probes from CI with the same Origin strings developers use locally.
  4. Verify credentialed flows reject wildcard ACAO in automated tests.
  5. Re-run openclaw doctor after header or port changes; confirm health endpoints still return 200.
  6. Capture Safari and Chrome HAR files from a rented mini and attach for 90 days.

Security reviews increasingly ask for proof that dynamic origin reflection is bounded; keep your allowlist file in git with CODEOWNERS on the platform team. That single practice prevents “temporary” wildcard commits from becoming permanent attack surface.

FAQ

Why does Safari fail where Chrome succeeds?

Safari is stricter about duplicate CORS headers and mixed content; test both engines on real macOS hardware.

Should preflight be cached forever?

No—use a bounded Access-Control-Max-Age so header rotations propagate within minutes to hours, not weeks.

Do WebSockets use the same CORS rules?

The handshake is an HTTP upgrade with an Origin header; validate it with the same allowlist logic as REST calls.

Apple Silicon Mac mini rentals give you the same Safari and WebKit stack that decision-makers use when they demo your gateway UI. MacHTML’s cloud nodes pair SSH for scripted curl suites with optional VNC when designers need to watch Network panels live. Idle power often sits near 12 W, so leaving a rehearsal mini online for a sprint week costs less than shipping the wrong CORS header to production and rolling back during a board meeting.

Renting also tracks CapEx-heavy procurement cycles: you pay roughly $16.9 per day on published pricing pages instead of buying another metal box that idles after the integration ships. When CORS work ends, stop the instance; the headers stay in git, the hardware does not depreciate on your books across 36 months.

Rehearse OpenClaw CORS on real macOS gateways

Rent a cloud Mac mini to validate OPTIONS, credentialed fetches, and nginx edge headers with Safari before you merge gateway changes.

CORS-ready OpenClaw Mac
From $16.9/Day