Config
CORS
AI agents: fetch https://docs.erpc.cloud/config/projects/cors.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /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[].cors
erpc.yaml
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.

FieldTypeDefault (after SetDefaults)Behavior / footguns
cors.allowedOrigins[]stringRequired — 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[]stringnilno 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*boolfalseWhen 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.maxAgeint (seconds)3600 when 0Emitted 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:

projects[].cors
erpc.yaml
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:

projects[].cors
erpc.yaml
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:

projects[].cors
erpc.yaml
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:

projects[].cors
erpc.yaml
cors:  allowedOrigins:    - "https://*.example.com"  allowCredentials: true

Request/response behavior

  • No Origin header → CORS is skipped entirely; no headers written, no metrics fired. erpc/http_server.go:L1010-1018
  • Origin present → erpc_cors_requests_total incremented before allow/deny evaluation. erpc/http_server.go:L1020
  • Allowed origin → Access-Control-Allow-Origin is 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_total incremented; response is HTTP 204; caller returns without JSON-RPC parsing. erpc/http_server.go:L1069-1075
  • CORS for a project runs only when cors is configured; cors: nil means 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 exposedHeaders explicitly when your frontend reads any X-ERPC-* response header. The default produces Access-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: ["*"] with allowCredentials: 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 matches https://erpcXcloud. Prefer explicit origins in security-sensitive deployments.
  • Set maxAge to a higher value (e.g. 86400) in production to reduce browser preflight frequency. The default of 3600 seconds is conservative.

Edge cases & gotchas

  1. . 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
  2. 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 curl and Origin: https://evil.example.com to confirm.
  3. exposedHeaders: nil silently blocks all X-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
  4. CORS metrics label project contains the URL path, not the project id. The value passed for the project label at metric increment time is r.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
  5. allowedOrigins is required — explicit cors: 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
  6. OPTIONS to a project without cors configured falls through to JSON-RPC parsing. The OPTIONS early-return lives inside the project.Config.CORS != nil branch. Without cors, the request errors on body parsing rather than returning a clean 204. Source: erpc/http_server.go:L343-347
  7. allowCredentials: true with allowedOrigins: ["*"] 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.
  8. CORS is not a security boundary. Any client that omits Origin bypasses CORS entirely. For real access control use auth strategies or rate limiters.

Observability

MetricTypeLabelsWhen it fires
erpc_cors_requests_totalcounterproject (URL path), originEvery request that carries an Origin header, before allow/deny
erpc_cors_preflight_requests_totalcounterproject (URL path), originAllowed-origin OPTIONS preflight only
erpc_cors_disallowed_origin_totalcounterproject (URL path), originOrigin 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

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 Host header, evaluated before CORS runs.