GuidesPLATFORMS

Handle CORS in Cloudflare Workers

Workers have no dashboard toggle or wrangler.toml setting for CORS; the fetch handler must answer OPTIONS requests and attach the headers in code. Every preflight and every actual response passes directly through your handler. Configuring Cloudflare Workers CORS means handling OPTIONS explicitly and setting the appropriate Access-Control response headers on the responses browsers must read.

There is no CORS switch

No dashboard toggle and no wrangler.toml key controls CORS for a Worker. Every response, including the preflight, comes from the fetch handler, so the handler must return the Access-Control headers. A Worker that ignores OPTIONS returns whatever the route returns, often a 405 or an HTML error page, and the preflight fails in the browser.

The minimal contract requires answering OPTIONS requests with the allowed methods and headers, and appending Access-Control-Allow-Origin to every response the browser must read.

Answer the preflight in the handler

Detect OPTIONS requests, verify the presence of Origin, Access-Control-Request-Method, and Access-Control-Request-Headers, and return an empty response carrying the allow headers. Echo the requested headers back in Access-Control-Allow-Headers.

Answer preflights in the fetch handler
function handleOptions(request: Request): Response {
  const origin = request.headers.get('Origin');
  const requestMethod = request.headers.get('Access-Control-Request-Method');
  const requestHeaders = request.headers.get('Access-Control-Request-Headers');

  if (origin !== null && requestMethod !== null && requestHeaders !== null) {
    // A real preflight: grant it and echo the requested headers
    return new Response(null, {
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
        'Access-Control-Max-Age': '86400',
        'Access-Control-Allow-Headers': requestHeaders,
      },
    });
  }

  // A plain OPTIONS request, not a CORS preflight
  return new Response(null, {
    headers: { Allow: 'GET, HEAD, POST, OPTIONS' },
  });
}

Non-preflight OPTIONS requests still deserve a sane Allow header such as GET, HEAD, POST, OPTIONS. Setting Access-Control-Max-Age lets the browser cache the preflight verdict so subsequent requests skip the preflight step.

Restrict origins with an allowlist

A wildcard allow-origin grants every website on the internet read access to the Worker's responses. Read the incoming Origin header, compare it against a list of permitted domains, and echo it back in Access-Control-Allow-Origin only when listed.

Echo only origins you trust, with Vary: Origin
const ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com'];

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method === 'OPTIONS') {
      return handleOptions(request);
    }

    const response = await handleRequest(request);

    // Recreate the response so the headers become mutable
    const result = new Response(response.body, response);
    const origin = request.headers.get('Origin');

    if (origin !== null && ALLOWED_ORIGINS.includes(origin)) {
      result.headers.set('Access-Control-Allow-Origin', origin);
      result.headers.append('Vary', 'Origin');
    }

    return result;
  },
};

Echoing a specific origin instead of * requires a Vary: Origin header so shared caches do not serve one origin's response to another. Credentialed requests are impossible with the wildcard, so cookie-based APIs must use the allowlist pattern.

The Hono shortcut

The Hono framework ships a cors middleware that implements the entire preflight and response header pattern. Calling app.use('/api/*', cors({ origin: 'https://app.example.com' })) handles preflights and sets response headers automatically. The origin option accepts a string, an array of strings, or a function that inspects the request origin and returns the value to emit. The allowHeaders, allowMethods, exposeHeaders, maxAge, and credentials options map directly to the matching Access-Control response headers.

Verify with curl

Deploy your Worker, then probe both the actual request and the OPTIONS preflight. Running wrangler dev serves the same handler locally on localhost:8787 for the same checks before deploying to production.

Probe the Worker locally and deployed
# wrangler dev serves the same handler on localhost:8787
BASE=http://localhost:8787

# Actual request: expect access-control-allow-origin
curl -i -H "Origin: https://app.example.com" $BASE/api/data

# Preflight: expect an empty 2xx with the allow headers
curl -i -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: content-type" \
  $BASE/api/data

Good questions.

Can I configure CORS in wrangler.toml?

No. There is no wrangler.toml key or Cloudflare dashboard setting for CORS. The fetch handler must answer OPTIONS preflights and set the Access-Control headers in code.

Why does my Worker return a 405 on CORS requests?

Browsers send an OPTIONS request before non-simple cross-origin calls. If the handler does not match or handle the OPTIONS method, the routing logic returns 405 Method Not Allowed, and the browser preflight fails.

Why do I need Vary: Origin when using an allowlist?

When the response echoes a specific origin instead of *, a shared cache can store the response for one origin and serve it to another. Vary: Origin instructs caches to key their stored responses on the Origin request header.

How do I test Worker CORS headers locally before deploying?

Run wrangler dev, which serves the handler locally at localhost:8787. Send curl requests with the Origin and Access-Control-Request-Method headers to verify the preflight and data endpoints return the expected Access-Control headers.