/config/projects/cors.llms.txt
CORS
Your backend calls always get through — eRPC only applies CORS to requests that carry an Origin header, which is every browser and nothing else. Add cors.allowedOrigins to a project and browser clients from your listed domains are allowed in; everyone else is quietly blocked by the browser. No config change needed for your servers, indexers, or scripts.
Quick taste
Illustrative, not a tuned production config — allow one origin and expose cache headers to browser JS:
projects: - id: main cors: allowedOrigins: - "https://app.example.com" # list every X-ERPC-* header your browser JS needs to read exposedHeaders: - "X-ERPC-Cache" - "X-ERPC-Duration"Agent reference
Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly:
Prompt Example #1: serve my dApp frontend directly from eRPC
My dApp frontend at https://app.example.com needs to call my eRPC endpoint directly from the browser. Configure CORS so only my origins are allowed, preflight requests are handled correctly, and local development on localhost keeps working. Work with my existing eRPC config. Read the full reference first: https://docs.erpc.cloud/config/projects/cors.llms.txt
Prompt Example #2: debug browser requests being blocked
Browser requests to my eRPC endpoint fail with CORS errors while curl works fine. Diagnose which CORS setting is wrong (origins, methods, headers, credentials) in my eRPC config and fix it, then explain what preflight response eRPC sends for my corrected config. Reference: https://docs.erpc.cloud/config/projects/cors.llms.txt
CORS — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
How it works
handleCORS (erpc/http_server.go:1008-1077 (opens in a new tab)) is called right after project resolution, once per inbound request.
No Origin header → always allowed. The code comment at erpc/http_server.go:1010-1018 explicitly states this is a soft deterrent against casual browser embedding, not a security boundary. Server-to-server callers, indexers, curl, and any client that omits Origin bypass CORS entirely and go straight to JSON-RPC processing. No headers are written and no metrics fire.
With an Origin header present, eRPC increments erpc_cors_requests_total, then evaluates each allowedOrigins pattern via WildcardMatch (eRPC's glob+boolean grammar). First match wins; pattern-match errors are logged at debug and the pattern is skipped.
Allowed origin: eRPC writes Access-Control-Allow-Origin set to the echoed request origin (never the literal *), plus Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Expose-Headers, and conditionally Access-Control-Allow-Credentials and Access-Control-Max-Age. An OPTIONS preflight then terminates with HTTP 204 before any JSON-RPC parsing happens.
Disallowed origin: eRPC omits all Access-Control-* headers but still processes the request — non-OPTIONS requests continue and produce a full JSON-RPC response; the browser enforces the block. OPTIONS returns a bare 204. erpc/http_server.go:1035-1051 (opens in a new tab)
CORS for a project runs only when cors is configured. When cors: nil, OPTIONS falls through to the JSON-RPC body parser and errors instead of returning a clean 204. erpc/http_server.go:343-347 (opens in a new tab)
Admin CORS is a separate surface evaluated before any admin request handling (erpc/http_server.go:295-301). When admin is configured but admin.cors is absent, SetDefaults auto-injects {allowedOrigins: ["*"], allowCredentials: false} — safe because admin endpoints require a secret token.
Origin pattern syntax. allowedOrigins entries are full WildcardMatch patterns: * = any sequence, ? = exactly one character, . = any single character (not a literal dot — see gotchas), | OR, & AND, ! NOT, parentheses for grouping. Precedence: NOT > AND > OR. Examples: "https://*.example.com" allows all subdomains; "https://app.example.com | https://staging.example.com" allows either of two explicit origins. Source: common/matcher.go:34-47 (opens in a new tab).
Config schema
All fields belong to projects[].cors (CORSConfig, common/config.go:L658-665). Defaults applied by common/defaults.go:L2824-2846. Validation at common/validation.go:L773-775.
| Field | Type | Default (after SetDefaults) | Behavior / footguns |
|---|---|---|---|
cors.allowedOrigins | []string | Required — validation errors if missing/empty (*.cors.allowedOrigins is required). ["*"] only for auto-injected admin CORS and programmatic configs that skip validation. | Each entry is a full WildcardMatch pattern matched against the Origin header. First match wins; match errors are logged and the pattern is skipped. Source: erpc/http_server.go:L1022-1033 |
cors.allowedMethods | []string | ["GET","POST","OPTIONS"] | Joined into Access-Control-Allow-Methods. No server-side enforcement; informational for the browser preflight. Source: common/defaults.go:L2828-2830 |
cors.allowedHeaders | []string | ["content-type","authorization","x-erpc-secret-token"] | Joined into Access-Control-Allow-Headers. Source: common/defaults.go:L2831-2837 |
cors.exposedHeaders | []string | nil — no default set; strings.Join(nil, ", ") emits "" so the browser exposes zero extra headers. | Joined into Access-Control-Expose-Headers. Footgun: omitting this field silently blocks browser JS access to every X-ERPC-* diagnostic header. eRPC emits the following response headers that are inaccessible to browser JS without an explicit list: X-ERPC-Cache, X-ERPC-Upstream, X-ERPC-Duration, X-ERPC-Attempts, X-ERPC-Upstream-Attempts, X-ERPC-Upstream-Retries, X-ERPC-Upstream-Hedges, X-ERPC-Network-Attempts, X-ERPC-Network-Retries, X-ERPC-Network-Hedges, X-ERPC-Consensus-Slots, X-ERPC-Consensus-Disputes, X-ERPC-Consensus-Low-Participants, X-ERPC-Cache-Attempts, X-ERPC-Cache-Retries, X-ERPC-Cache-Hedges. Enumerate desired headers explicitly — glob shorthand is not part of the CORS spec. Sources: erpc/http_server.go:L1057, erpc/http_server.go:L1088-1204 |
cors.allowCredentials | *bool | false | When true, emits Access-Control-Allow-Credentials: true. Footgun: browsers reject allowCredentials: true combined with allowedOrigins: ["*"]; the response is blocked entirely. Source: common/defaults.go:L2838-2840 |
cors.maxAge | int (seconds) | 3600 when 0 | Emitted as Access-Control-Max-Age only when > 0. Controls browser preflight cache TTL. Source: common/defaults.go:L2841-2843 |
admin.cors uses the same CORSConfig struct. When admin is present and cors is nil, SetDefaults injects {allowedOrigins: ["*"], allowCredentials: false} then runs SetDefaults. Source: common/defaults.go:L769-787.
Worked examples
1. Minimal single-origin setup. The simplest production config: one explicit origin allowed, no extras needed. Browser preflight is answered with 204; all server-to-server calls are unaffected:
cors: allowedOrigins: - "https://app.example.com"2. Frontend dashboard that reads eRPC diagnostic headers. Browser JS cannot read X-ERPC-Cache or X-ERPC-Duration without explicit exposedHeaders. Add every header your frontend needs — glob shorthand (X-ERPC-*) is not part of the CORS spec and most browsers ignore it:
cors: allowedOrigins: - "https://dashboard.example.com" exposedHeaders: - "X-ERPC-Cache" - "X-ERPC-Upstream" - "X-ERPC-Duration" - "X-ERPC-Attempts"3. Multi-environment with boolean OR pattern. When you have a production and staging origin, use the boolean OR syntax instead of two separate entries — the matcher evaluates them as a single expression:
cors: allowedOrigins: - "https://app.example.com | https://staging.example.com" exposedHeaders: - "X-ERPC-Cache"4. Wildcard subdomain with credentials. Allow any subdomain of your company domain and send cookies/auth headers. Never combine allowedOrigins: ["*"] with allowCredentials: true — browsers will reject the response:
cors: allowedOrigins: - "https://*.example.com" allowCredentials: trueRequest/response behavior
- No
Originheader → CORS is skipped entirely; no headers written, no metrics fired.erpc/http_server.go:L1010-1018 - Origin present →
erpc_cors_requests_totalincremented before allow/deny evaluation.erpc/http_server.go:L1020 - Allowed origin →
Access-Control-Allow-Originis set to the echoed request origin, never the literal*.erpc/http_server.go:L1053-1065 - Disallowed origin → no
Access-Control-*headers; non-OPTIONS request continues to JSON-RPC processing; OPTIONS returns bare 204.erpc/http_server.go:L1035-1051 - OPTIONS + allowed origin →
erpc_cors_preflight_requests_totalincremented; response is HTTP 204; caller returns without JSON-RPC parsing.erpc/http_server.go:L1069-1075 - CORS for a project runs only when
corsis configured;cors: nilmeans OPTIONS falls through to JSON-RPC body parsing and errors.erpc/http_server.go:L343-347 - CORS for a project can only run after project resolution succeeds; an unknown project id → HTTP 404 with no CORS headers (browsers may surface this as a CORS error rather than a 404).
erpc/http_server.go:L326-347 - All pattern-match errors are logged at debug and the pattern is skipped — never panics.
erpc/http_server.go:L1022-1033
Best practices
- Always list
exposedHeadersexplicitly when your frontend reads anyX-ERPC-*response header. The default producesAccess-Control-Expose-Headers: "", silently blocking browser JS access to all eRPC diagnostic headers:X-ERPC-Cache,X-ERPC-Upstream,X-ERPC-Duration,X-ERPC-Attempts,X-ERPC-Upstream-Attempts,X-ERPC-Upstream-Retries,X-ERPC-Upstream-Hedges,X-ERPC-Network-Attempts,X-ERPC-Network-Retries,X-ERPC-Network-Hedges,X-ERPC-Consensus-Slots,X-ERPC-Consensus-Disputes,X-ERPC-Consensus-Low-Participants,X-ERPC-Cache-Attempts,X-ERPC-Cache-Retries,X-ERPC-Cache-Hedges. (erpc/http_server.go:L1088-1204) - Never use
allowedOrigins: ["*"]withallowCredentials: true. eRPC will emit both headers; browsers reject the combination entirely. Use an explicit origin list when credentials are required. - Use the boolean OR pattern (
"https://app.example.com | https://staging.example.com") rather than two entries when combining origins — both forms work but the single-expression form is easier to audit. - Verify disallowed-origin behavior with
curl --header "Origin: https://evil.example.com" .... eRPC still returns a JSON-RPC response; only the browser enforces the block. CORS is not an access-control boundary — pair it with an auth strategy for real security. - Be aware that
.in wildcard patterns matches any single character, not a literal dot.allowedOrigins: ["https://erpc.cloud"]also matcheshttps://erpcXcloud. Prefer explicit origins in security-sensitive deployments. - Set
maxAgeto a higher value (e.g.86400) in production to reduce browser preflight frequency. The default of 3600 seconds is conservative.
Edge cases & gotchas
.matches any single character, not a literal dot."https://erpc.cloud"also matches"https://erpcXcloud". No escape syntax exists. Review each pattern for unintended substitutions. Source:common/matcher.go:L34-47- Disallowed origins are not rejected server-side. Non-OPTIONS requests proceed and receive a full JSON-RPC response; only the browser enforces the block. Test with
curlandOrigin: https://evil.example.comto confirm. exposedHeaders: nilsilently blocks allX-ERPC-*headers from browser JS.strings.Join(nil, ", ")→"". Browser interprets as zero extra headers. eRPC writes these headers on every response (all blocked without an explicit list):X-ERPC-Cache,X-ERPC-Upstream,X-ERPC-Duration,X-ERPC-Attempts,X-ERPC-Upstream-Attempts,X-ERPC-Upstream-Retries,X-ERPC-Upstream-Hedges,X-ERPC-Network-Attempts,X-ERPC-Network-Retries,X-ERPC-Network-Hedges,X-ERPC-Consensus-Slots,X-ERPC-Consensus-Disputes,X-ERPC-Consensus-Low-Participants,X-ERPC-Cache-Attempts,X-ERPC-Cache-Retries,X-ERPC-Cache-Hedges. Enumerate by name — no wildcard shorthand is accepted by current browsers. Sources:erpc/http_server.go:L1057,erpc/http_server.go:L1088-1204- CORS metrics label
projectcontains the URL path, not the project id. The value passed for theprojectlabel at metric increment time isr.URL.Path(e.g./main/evm/1). Cardinality grows with distinct request paths; you cannot group by project id from these counters alone. Source:erpc/http_server.go:L1020 allowedOriginsis required — explicitcors:block without it fails startup with*.cors.allowedOrigins is required. The["*"]fallback only applies to auto-injected admin CORS. Source:common/validation.go:L773-775- OPTIONS to a project without
corsconfigured falls through to JSON-RPC parsing. TheOPTIONSearly-return lives inside theproject.Config.CORS != nilbranch. Withoutcors, the request errors on body parsing rather than returning a clean 204. Source:erpc/http_server.go:L343-347 allowCredentials: truewithallowedOrigins: ["*"]is not rejected at startup, only at browser runtime. eRPC will send the conflicting headers; browsers refuse the response with a CORS error. Validate this combination manually.- CORS is not a security boundary. Any client that omits
Originbypasses CORS entirely. For real access control use auth strategies or rate limiters.
Observability
| Metric | Type | Labels | When it fires |
|---|---|---|---|
erpc_cors_requests_total | counter | project (URL path), origin | Every request that carries an Origin header, before allow/deny |
erpc_cors_preflight_requests_total | counter | project (URL path), origin | Allowed-origin OPTIONS preflight only |
erpc_cors_disallowed_origin_total | counter | project (URL path), origin | Origin present but no pattern matched |
Note: the project label value is r.URL.Path (e.g. /main/evm/1), not the project id. Cardinality scales with distinct request paths.
Source code entry points
erpc/http_server.go:L1008-L1077(opens in a new tab) —handleCORS: full algorithm; no-origin fast path, wildcard matching loop, header writing, 204 preflight terminationcommon/config.go:L658-L665(opens in a new tab) —CORSConfigstruct definitioncommon/defaults.go:L2824-L2846(opens in a new tab) —CORSConfig.SetDefaults: method/header defaults,allowCredentialsfalse,maxAge3600common/validation.go:L773-L775(opens in a new tab) —CORSConfig.Validate:allowedOriginsrequired checkcommon/matcher.go:L34-L47(opens in a new tab) —WildcardMatchtokenizer including the.= any-char rulecommon/defaults.go:L769-L787(opens in a new tab) — admin CORS auto-injection (allowedOrigins: ["*"])erpc/http_server_test.go:L3239-L3376(opens in a new tab) — CORS allowed/disallowed/preflight test suite
Related pages
- Auth — the correct tool for access control; pair with CORS to stop real attackers, not just browsers.
- Rate limiters — per-IP or per-user limits complement CORS for public-facing projects.
- Projects overview — full per-project config including method filtering, auth strategies, and rate-limit budgets.
- Server config — domain aliasing rules that pre-select a project via
Hostheader, evaluated before CORS runs.