Usage
Last updated 2026-09-18
This page is the manual for the instance at https://corx.envx.cn: the four ways to call the proxy, every control parameter, the auth tiers, what the cache does, the limits you will hit and the security model behind it. README.md in the repository stays the source of truth for deploying your own copy.
Calling the proxy
Every shape below resolves to the same request through the same pipeline — auth, SSRF guards, upstream injection, cache, logging. CORS preflight (`OPTIONS`) is answered before the proxy runs, so browser `fetch` just works.
Query parameter (recommended)
The shape this documentation uses everywhere: the target URL, percent-encoded, in `?url=`. Works on `/fetch` and on every other proxy route.
fetch("https://corx.envx.cn/fetch?url=" + encodeURIComponent("https://api.example.com/data"))Path
The target appended after `/proxy/`. Easy to read and to paste into a browser; the target's own query string survives after the first `?`.
fetch("https://corx.envx.cn/proxy/https://api.example.com/data")Bare path
The same as `/proxy/`, one segment shorter: any path that is not a CORX page and looks like a URL is proxied.
fetch("https://corx.envx.cn/https://api.example.com/data")Subdomain mode
When the deployment has a wildcard zone, a target gets a hostname of its own: dots become hyphens and hyphens double (`api.example.com` → `api-example-com.<zone>`). The request's query string is the target's query string, so the `corx-*` names are stripped back off.
fetch("https://api-example-com.<zone>/data")GET and HEAD responses are cached and counted; every other method passes straight through, uncached. A caller-supplied target keeps its own query string: the target's own `key`, `ttl` or `callback` parameters are forwarded untouched, and CORX only consumes names it owns.
Copy-paste examples for fetch, axios and ky — and for Cloudflare Pages, Vercel and Netlify — live on the snippets page.Or test a URL's CORS from your browser with the CORS tester.
The corx-* namespace
`corx-*` is CORX's namespace: these parameters are consumed by the proxy and never reach the target. Everything else belongs to the target and is forwarded untouched. A `corx-*` name that is not in this table is a 400, never a param quietly forwarded upstream.
| Parameter | Effect |
|---|---|
corx-ttlcorx-ttl=300 | Cache TTL in seconds for this GET response. Capped by the deployment's maximum (and by a public key's own TTL) so no caller can pin an entry for a day. |
corx-no-cachecorx-no-cache=1 | Bypass the R2 cache for this request: fetch upstream, return, do not store. Range requests, JSONP and credentialed requests bypass it anyway. |
corx-keycorx-key=corx_… | The API key for this request. Equivalent to `X-Api-Key` or `Authorization: Bearer`. Public-tier keys cannot control the cache. |
corx-callbackcorx-callback=handleData | JSONP: wrap an `application/json` body as `fn(<json>);` (max 2 MiB) for a `<script>` tag when CSP blocks `fetch`. JSONP never caches. |
corx-charsetcorx-charset=utf-8 | Re-decode a text, JSON or XML response with this label and re-emit it as UTF-8 — the fix when an upstream mislabels its charset. An unknown label is a 400. |
corx-wrapcorx-wrap=json | Wrap a text body as `{"contents":"…"}` with `application/json`, so `r.json()` works for HTML too. Binary responses are refused with a 400; the wrapper is part of the cache key. Both transforms are a no-op on a bodyless status (`204`, `304`, …): the status and its validators pass through unchanged. |
corx-schemecorx-scheme=http | Subdomain mode: the target scheme. Defaults to `https`; `http` is the only other accepted value. |
corx-portcorx-port=8443 | Subdomain mode: the target port (1–65535), appended unless it is the scheme's default (80 for http, 443 for https). |
Subdomain and path modes are the two places where the proxy request's query is also the target's, so the `corx-*` names are stripped back off there. A target that genuinely needs a `corx-*` parameter is best addressed with `?url=`.
Authentication
Three ways in — roughly the order a self-hosted deployment turns them on.
API key (per caller)
Created in the console as `corx_<random>` and stored only as a SHA-256 hash — the raw value is shown once. A key can carry allowed origins, a per-minute rate limit, a cache TTL, keyless grants, allowed hosts, SSRF-check opt-outs and upstream injection. Send it in any of the forms below.
X-Api-Key: corx_…
Authorization: Bearer corx_…
?corx-key=corx_…All three are equivalent; use whichever survives your client or tooling. The credential CORX authenticated with is never forwarded upstream: a corx-shaped `X-Api-Key` or `Authorization: Bearer corx_…` is always consumed, even when the key is unknown. Any other value in `X-Api-Key` is your own upstream credential (BYOK) and is forwarded untouched — present the CORX key via `Authorization: Bearer corx_…` or `?corx-key=` alongside it (a corx-shaped `X-Api-Key` takes precedence).
Keyless origin grants
Enable keyless access on a key and browsers from its allowed origins call the proxy without carrying the key at all. The grant matches the `Origin` header (or the `Referer`'s origin for same-origin GETs) and is metered per visitor IP, so one embedded site cannot drain the key. Origins are canonicalized on save (lowercase host, default port dropped), and the one wildcard form is a loopback port — `http://localhost:*` covers any localhost port, so a dev server on a random port needs a single entry. Host wildcards (`https://*.example.com`) and regexes are rejected. An origin is a convenience, not a credential — scripts can forge it — so pair it with allowed hosts and a rate limit.
Public tier
A hosted instance may publish a shared key on its landing page. It is deliberately reduced: GET and HEAD only, daily quotas per calling site / per target host / per instance, no cache control, no injection and no subdomain mode; `Cookie` and `Authorization` are stripped before forwarding. Fine for public data, demos and prototypes.
Where the key goes — and where it must not
Server-side only. A key in a browser bundle, a public repository or a page source is a key you have given away; a site that needs to call the proxy from a browser should use a keyless origin grant (or the public key, if the data really is public). Never send credentials or personal data through a shared instance at all.
Upstream credentials
A key can hold the upstream API credentials your app needs and attach them server-side: the browser never receives the value. Rules decide what goes where by target host, and a variable you expose can also be referenced by the caller itself.
How a credential is attached
Variables (`NAME=value`) hold the values; header and query rules use them. An `@host` line scopes the rules below it, so one key can carry a different credential per upstream — including the usual shape, the same header name with a different value per host (`Authorization` for two vendors). Header rules set or remove request headers (`Authorization: Bearer ${VENDOR_KEY}`, `!X-Debug`); query rules do the same with URL parameters (`api_key = ${VENDOR_KEY}`) for APIs that authenticate that way. Rules always win over what the caller sent, so a browser cannot spoof an injected header, and which rule applies is decided by the target host, not by the caller.
Letting the caller reference a variable
Exposure is per variable — `client: true` on the Admin API, the **Client-referencable variables** field in the console — and it names the hosts the caller may reference it toward. A caller then writes `${VENDOR_KEY}` in a header or query param of its own request; the proxy substitutes the value before forwarding. The caller only ever learns the name it wrote, never the value, and a variable you do not expose stays invisible to it.
Two scopes, and both must allow the host
A rule's `@hosts` says where *that rule* applies; a variable's own hosts say where *that variable* may ever resolve. The second bounds the first: a rule that could resolve a scoped variable toward another host is rejected at save time — including a key-level rule, because it can reach every allowed host. Patterns are the same everywhere: an exact host, `*.suffix` (label boundary), or `*`, so `*.vendor.com` covers `api.vendor.com` and nothing covers the apex `vendor.com` implicitly. Client references are evaluated per redirect hop, so a variable scoped to one host does not resolve after a redirect to another.
Order, and what happens when nothing matches
On the way out, caller references resolve first and the key's rules run afterwards, removes before sets. A rule therefore always beats what the caller asked for — `!X-Debug` deletes it, a `set` replaces it — so a browser cannot spoof an injected header. Anything the proxy will not resolve (unknown name, private variable, host outside the scope) is forwarded exactly as written. That keeps the feature additive (a key with no exposed variable behaves exactly as before) and makes an unexposed name indistinguishable from a nonexistent one, so callers cannot probe what a key holds. Headers CORX owns — a corx-shaped `X-Api-Key`, `X-Admin-Token` and the hop-by-hop set — are dropped before anything resolves; a non-corx `X-Api-Key` (including a `${VAR}` reference) is the caller's own upstream credential and is forwarded. A vendor requiring one of these names (Anthropic's `x-api-key`) is therefore covered either way: BYOK from a trusted server, or a rule when the browser must never hold the secret.
One key, three upstreams
The console's key page edits the variables as rows (each with a Client toggle and a host scope) next to the Header / Query / Response rules fields and the allowed-hosts field; the Admin API takes them as `vars` (with `client` and `hosts` per variable), `headerRules`, `paramRules`, `responseRules` and `allowedHosts`.
Variables
OPENAI_KEY=sk-…
ANTHROPIC_KEY=sk-ant-…
VENDOR_KEY=vendor-…Client-referencable variables
@api.vendor.com
VENDOR_KEYHeader rules
@api.openai.com
Authorization: Bearer ${OPENAI_KEY}
@api.anthropic.com
x-api-key: ${ANTHROPIC_KEY}
@api.vendor.com
Authorization: Bearer ${VENDOR_KEY}Allowed target hosts (required with any injection)
api.openai.com, api.anthropic.com, api.vendor.comEvery `@host` must fall inside the allowed-hosts list, and the list is mandatory once anything is injected — that is the guard that stops a caller from pointing your credential at a host of their choosing. A rule outside the list is never attached.
What a caller's reference becomes
For the example above: `VENDOR_KEY` is exposed for `api.vendor.com` only, `PRIVATE_KEY` is not exposed, and a header rule sets `Authorization` for `api.vendor.com`.
| Caller sends | Target host | Upstream receives |
|---|---|---|
x-vendor: Bearer ${VENDOR_KEY}Exposed, and the host is inside the variable's scope → resolved. | api.vendor.com | x-vendor: Bearer vendor-… |
x-vendor: Bearer ${VENDOR_KEY}Inside the key's allowlist, but outside the variable's scope → left as written. | api.other.com | x-vendor: Bearer ${VENDOR_KEY} |
x-private: ${PRIVATE_KEY}Not exposed to callers at all → left as written. | api.vendor.com | x-private: ${PRIVATE_KEY} |
x-typo: ${NOPE}No such variable → left as written, exactly like a private one. | api.vendor.com | x-typo: ${NOPE} |
Authorization: Bearer ${VENDOR_KEY}The rule applies afterwards → its value wins over the caller's reference. | api.vendor.com | Authorization: Bearer <what the rule sets> |
What it is for
The common case is one front end calling several providers — an LLM at one vendor, payments or maps at others — each with its own key and no secret in the page. It also covers an API that authenticates with a query parameter instead of a header, and the same upstream with different keys per environment (one key per environment, or a variable per deployment). With client references the page can even choose which of several credentials to use without ever holding one — the pattern a browser-side app needs once it talks to more than one provider.
One key or several
Everything above puts every upstream in a single key — one allowed-hosts union, one rate limit and one allowed-origins list shared by all of them, and one secret store to rotate. Split into one key per upstream when you want separate limits or origins, when a leak should reach only one vendor, or when the same host needs different credentials on different paths (rules are scoped by host, not by path). Keyless access grants an origin exactly one key, so a page that calls several upstreams without carrying a key gets all of them from that key's rules — and, for the variables you exposed, can reference them.
Cache and safety
Secrets are encrypted at rest when `INJECTION_KEK` is set, masked everywhere they are read (console, logs, playground preview), and the request log stores the pre-injection URL. A key with header rules never reads or writes the R2 cache, because its responses are key-specific, while a key that injects only query parameters still caches under the effective URL. A key with any client-referencable variable does not use the shared cache at all: the reference is caller-supplied and a redirect can resolve it on a later hop, so no response through that key can be proven free of a secret. The public tier cannot inject at all — it is GET/HEAD only — so upstream credentials mean a self-hosted instance and a standard key. Treat an exposed variable as public to anyone who can call the key.
Caching
GET responses are cached in R2 and served from the edge, so a repeated request usually never reaches upstream. `X-Corx-Cache: HIT|MISS` on every response says which path it took.
Cache markers
`X-Corx-Cache` is the header to watch while debugging: `MISS` means upstream answered (the response is stored only when it is cacheable), `HIT` means R2 answered. `X-Corx-Target` names the upstream host, and `X-Corx-Latency-Ms` is the time the proxy spent.
TTL
The deployment's default TTL applies unless `?corx-ttl=` lowers or raises it, up to the cap. A key can pin its own default TTL, or `0` to never store; the public tier cannot set a TTL at all.
What bypasses the cache
Always bypassing the shared cache: non-GET/HEAD methods, requests carrying `Authorization` or `Cookie`, `?corx-no-cache=1`, JSONP (`corx-callback`), Range requests, and keys that inject upstream headers. Keys with response header rules do cache — their resolved rules are part of the cache key, so a rewritten response is never served to another key.
Limits and errors
Two independent limits protect an instance: a per-minute rate limit per key (or per IP for anonymous calls), and the public tier's daily quotas.
The per-minute limit is counted on the key, or on the caller IP without one. Cache hits are free — only misses are metered — and the D1-backed checks fail open during a database incident rather than taking the proxy down.
A public key's daily counters run per calling site, per target host and for the instance as a whole, in UTC days. Cache hits count as well — the quota is about requests, not upstream load. `X-Corx-Quota-{Origin,Host,Day}-{Limit,Remaining}` reports where you stand.
Over a public daily quota the proxy answers `429` with `Retry-After` (seconds to UTC midnight) and a `{ error, scope, limit, resetAt }` body; the per-minute rate limit answers a plain `429` `{ error }` with no retry hint. Ordinary responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Every machine-facing path (the proxy, `/api/*`, `/health`) answers errors as JSON `{ error }`; browser pages get a branded HTML document.
Security
CORX is a CORS proxy, so it is a man in the middle by construction: whoever operates an instance can read, change and replay everything passing through it. The guards below reduce what an untrusted caller can reach; they do not make a shared instance safe for secrets.
SSRF guards: private, link-local, CGNAT, multicast and reserved IP literals are blocked, the host is resolved over DoH and re-checked so a name cannot rebind to a private address, and a D1 blocklist covers whole hosts and their subdomains. A trusted key can opt out of the IP/hostname and DNS checks; the blocklist and Cloudflare's own rules are never bypassed.
Header hygiene: hop-by-hop and proxy-owned headers (`Host`, `Connection`, `X-Forwarded-For`, `CF-*`, …) are stripped on the way in and out, `Set-Cookie` is never forwarded, and the public tier strips `Cookie` and `Authorization` before forwarding.
What a proxy can see: the target URL, the request and response bodies, and the caller's IP, `Origin` and country. A hosted instance logs requests and rolls them into a per-day aggregate; a self-hosted deployment chooses whether and how long (`LOG_REQUESTS`, `LOG_RETENTION_DAYS`).
So the honest way to run CORX is self-hosting: one MIT-licensed Worker in an account you control. A hosted instance is a shared, best-effort demo — read the terms of use and the trust model before sending it traffic.
Self-hosting
Every limit on this page — quotas, rate limits, retention, allowed hosts — is a setting on a Worker you control once you deploy your own copy. The README covers the ten-minute deployment (D1 + R2 + `wrangler deploy`); CONTRIBUTING.md covers local development and the checks a change has to pass.