CORS Anywhere Alternative: A Free Cloudflare Worker CORS Proxy (POST and Any URL)
Published: June 22, 2026
Infra Guide for AI Tool Builders
A lot of people look for a cors-anywhere alternative or replacement they can run themselves. Something general: give it any URL, any method, including a POST with a JSON body, and it adds the CORS headers and passes the call through. The well known ones are the Node project cors-anywhere and its Cloudflare port cloudflare-cors-anywhere.
This post gives you that, written my way: a small, free Cloudflare Worker that works as a general CORS proxy. You run your own copy. No server, no custom domain, free Cloudflare account is enough (100k requests per day). The full code is below, and it is also in a GitHub repo you can clone or hand to your AI coder.
If you have not read the main CORS post yet, start there: Free CORS Proxy for Yahoo Finance and Any API. It explains what CORS is in plain words, the preflight nuance, the no-cors trap, and when to use a Worker vs a Vercel serverless function vs a FastAPI backend. This post assumes you already know why you need a proxy and just want the general purpose one.
How this is different from the file proxy
The Worker in the main post is on purpose a narrow one. It was built to pull data files into a browser app (GitHub, Google Drive, Dropbox, and Yahoo Finance if you add it). So it only does GET, and it only fetches from a small allow list of file hosts.
This one is the general version. The difference is scope:
- It passes through any method, so POST, PUT, PATCH, DELETE all work, not just GET.
- It forwards the request body, so a JSON POST goes through.
- It can fetch any URL (you decide how wide to open it).
Same idea, bigger job. If you just need to pull files into the browser, the file proxy is simpler and safer. If you need to proxy a real API call, including POST, use the one here.
How you call it
Once it is deployed, put your target URL, URL-encoded, in the url query parameter:
https://your-worker.workers.dev/?url=<encoded-target-url>
The older ?uri= spelling also works, so forks that expect ?uri= will not break.
A GET call:
https://your-worker.workers.dev/?url=https://query1.finance.yahoo.com/v8/finance/chart/AAPL
A POST call from browser JavaScript, with a JSON body:
fetch("https://your-worker.workers.dev/?url=" + encodeURIComponent("https://api.example.com/data"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hello: "world" }),
});
The Worker reads the body, sends it on to the target, gets the response, adds the CORS headers, and streams it back to your browser.
Locking it down (read this before you rely on it)
Here is the thing to understand first. A CORS proxy is called from the browser. So anything the browser has to send to the proxy, like a key, is visible to anyone who opens the network tab on your page. You cannot truly keep a secret in a browser-called proxy. Everything below is about raising the bar and capping the damage, not making it airtight.
Out of the box the code below is fully open, so it just works while you test. Turn the locks on before you put it into real use. There are four.
1. Origin allow list
"Origin" is the website the request comes from. It is a domain, like https://yourapp.com. It is not anyone's laptop IP. When browser JavaScript makes a cross-site call, the browser automatically attaches an Origin header, and page JavaScript cannot change or fake it. So if you list your own domains, only pages served from those domains can use your proxy from a browser. The Worker checks this and rejects others before it fetches anything, so a blocked call costs you nothing.
The honest caveat: this only stops other websites. A script or another server is not a browser, so it can send any Origin it likes. So origin lock stops a random site from piggybacking on your proxy in their browser app, which is the main abuse. It does not stop a determined script.
2. Target allow list
This is which destination hosts the proxy is allowed to fetch. Narrowing this is the strongest single control. If your app only ever talks to one API, list just that host. Even if someone gets past everything else, they can only reach the hosts you allow, so your proxy is useless as a general tool for them.
3. API key (two ways)
If you set a Worker secret named API_KEY, every caller must send a matching X-API-Key header. There are two clean ways to use it:
- For a script or a server caller, send the key in the
X-API-Keyheader. This is the normal way, and for a script there is no CORS at all, so it is a good fit. - For a browser app, do not hardcode the key in your frontend code, because it would be visible. Instead, give the user a small box to paste their own key. The page then sends whatever they typed as the
X-API-Keyheader. The key never lives in your source.
Because a browser-called proxy cannot truly hide a secret, the API key mostly helps for script callers and raises the bar for casual abuse. For browser use, the origin allow list is the natural lock.
4. Rate limit, and the free-tier safety net
You can add Cloudflare's rate-limit binding (a few lines in wrangler.toml, shown below) to cap how many calls one IP can make. On top of that, the free Cloudflare plan caps you at 100k requests per day. If an open proxy ever gets hammered, the worst case is that it stops working for the day once the cap is hit. It fails closed. You do not get a surprise bill, which is the opposite of leaving something open on a pay-per-use cloud.
One more safety point: Cloudflare Workers cannot fetch private or localhost addresses, so this proxy can only reach the public web. It cannot be pointed at your internal network.
Deploy it
Two ways.
Dashboard, no tools needed:
- Go to dash.cloudflare.com and open Workers and Pages, then Create, then Create Worker.
- Give it a name, click Deploy, then Edit code.
- Delete the default code, paste in the Worker code below, click Save and Deploy.
- Your URL shows at the top:
https://your-name.workers.dev.
CLI, or let your AI coder do it:
CLOUDFLARE_API_TOKEN=your_token npx wrangler deploy
You need one Cloudflare API token with "Edit Cloudflare Workers" permission. Create it at dash.cloudflare.com/profile/api-tokens using the "Edit Cloudflare Workers" template.
To turn on the API key lock:
npx wrangler secret put API_KEY
The Worker code
Save as cors-proxy-worker.js. To lock it down, edit ALLOWED_ORIGINS and ALLOWED_TARGETS near the top (the comments tell you how). Left as is, it is fully open.
/**
* Generic CORS Proxy - Cloudflare Worker (a cors-anywhere style proxy)
*
* Your browser calls this Worker, the Worker fetches the target URL on the
* server side (where CORS does not apply), adds CORS headers, and sends the
* result back. It passes through any method (GET, POST, PUT, PATCH, DELETE) and
* forwards the request body and headers.
*
* Call it: https://your-worker.workers.dev/?url=<encoded-target-url>
* The older "?uri=" spelling also works.
*/
// 1. Which websites may call this proxy. Use ["*"] to allow any site.
// To lock it to your own apps: ["https://yourapp.com", "https://www.yourapp.com"]
const ALLOWED_ORIGINS = ["*"];
// 2. Which destination hosts this proxy may fetch. Use ["*"] for any URL.
// To lock it down: ["api.example.com", "query1.finance.yahoo.com"]
const ALLOWED_TARGETS = ["*"];
// Methods passed through to the target.
const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
function originAllowed(origin) {
if (ALLOWED_ORIGINS.includes("*")) return true;
if (!origin) return false;
return ALLOWED_ORIGINS.includes(origin);
}
function targetAllowed(targetUrl) {
if (ALLOWED_TARGETS.includes("*")) return true;
try {
const host = new URL(targetUrl).hostname.toLowerCase();
return ALLOWED_TARGETS.some(d => host === d || host.endsWith("." + d));
} catch {
return false;
}
}
function corsHeaders(origin) {
const open = ALLOWED_ORIGINS.includes("*");
const headers = {
"Access-Control-Allow-Origin": open ? "*" : (origin || ""),
"Access-Control-Allow-Methods": ALLOWED_METHODS.join(", "),
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, Accept",
"Access-Control-Max-Age": "86400",
};
if (!open) headers["Vary"] = "Origin";
return headers;
}
function jsonResponse(obj, status, origin) {
return new Response(JSON.stringify(obj, null, 2), {
status,
headers: { "Content-Type": "application/json", ...corsHeaders(origin) },
});
}
export default {
async fetch(request, env) {
const origin = request.headers.get("Origin");
// Answer the browser preflight first.
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders(origin) });
}
// Lock 1: origin allow list. Reject early so disallowed sites cost nothing.
if (!originAllowed(origin)) {
return jsonResponse({ error: "Origin not allowed" }, 403, origin);
}
// Lock 3: optional API key (set a Worker secret named API_KEY to turn on).
if (env && env.API_KEY) {
if (request.headers.get("X-API-Key") !== env.API_KEY) {
return jsonResponse({ error: "Missing or invalid X-API-Key" }, 401, origin);
}
}
// Lock 4: optional rate limit (only runs if the binding is configured).
if (env && env.RATE_LIMITER) {
const ip = request.headers.get("CF-Connecting-IP") || "anon";
const { success } = await env.RATE_LIMITER.limit({ key: ip });
if (!success) return jsonResponse({ error: "Rate limit exceeded" }, 429, origin);
}
// Read the target URL from ?url= (or ?uri=).
const reqUrl = new URL(request.url);
const targetUrl = reqUrl.searchParams.get("url") || reqUrl.searchParams.get("uri");
if (!targetUrl) {
return jsonResponse({
name: "Generic CORS Proxy",
usage: `${request.method} ${reqUrl.origin}/?url=<encoded-target-url>`,
note: "Put the target URL, URL-encoded, in the url (or uri) query parameter.",
}, 400, origin);
}
// Lock 2: target allow list.
if (!targetAllowed(targetUrl)) {
return jsonResponse({ error: "Target host not allowed" }, 403, origin);
}
// Build the upstream request. Copy method, body, and most headers, but drop
// headers that should not be forwarded (our own X-API-Key, and the
// Cloudflare / origin headers the platform adds).
const upstreamHeaders = new Headers(request.headers);
["host", "origin", "referer", "x-api-key",
"cf-connecting-ip", "cf-ipcountry", "cf-ray", "cf-visitor", "x-forwarded-proto"]
.forEach(h => upstreamHeaders.delete(h));
const hasBody = !["GET", "HEAD"].includes(request.method);
let upstream;
try {
upstream = await fetch(targetUrl, {
method: request.method,
headers: upstreamHeaders,
body: hasBody ? request.body : undefined,
redirect: "follow",
});
} catch (err) {
return jsonResponse({ error: "Upstream fetch failed", message: err.message }, 502, origin);
}
// Send the response back, adding our CORS headers and removing headers that
// would block the browser from reading or embedding it.
const respHeaders = new Headers(upstream.headers);
const ch = corsHeaders(origin);
Object.keys(ch).forEach(k => respHeaders.set(k, ch[k]));
respHeaders.set("Access-Control-Expose-Headers", "*");
respHeaders.delete("content-security-policy");
respHeaders.delete("x-frame-options");
return new Response(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers: respHeaders,
});
},
};
And the config, save as wrangler.toml in the same folder. The rate limit block is commented out, so it is optional. The Worker only runs the rate check if the binding is present, so it is safe to leave off.
name = "cors-proxy"
main = "cors-proxy-worker.js"
compatibility_date = "2024-09-01"
# Optional rate limit. Uncomment to turn it on. limit = requests, period = 10 or 60 seconds.
# [[unsafe.bindings]]
# name = "RATE_LIMITER"
# type = "ratelimit"
# namespace_id = "1001"
# simple = { limit = 100, period = 60 }
[observability.logs]
enabled = true
GitHub repo
The same code, ready to clone or fork: github.com/amararun/cloudflare-cors-proxy. It has the Worker, the wrangler.toml, and a short README.
The real shortcut
Here is the honest bit. If you are already working with an AI coder like Claude or Cursor, you do not need to copy any of this. Point it at this page or at the GitHub repo and say "deploy this CORS proxy for me, lock it to my domain." It will create the Worker, set the token, and give you back a live URL in a couple of minutes. The code above is here so you can read it and so the agent has something exact to work from.
That is the wider point of this site. Stop hunting through search results for snippets. Ask your agent, point it at a known good source, and let it do the wiring. There are 30+ open tools, APIs, and guides here built around that idea.
Related
- Free CORS Proxy for Yahoo Finance and Any API - the main CORS post. CORS explained simply, plus the file-download proxy for GitHub, Drive, Dropbox, and Yahoo Finance.
- Part 1: AI Coder
- Part 2: Deployment and Hosting
- Part 3: The 18 Common Security Mistakes and How to Fix Them