Back to Blog
Lesson 40 of the Cloudflare: Cloudflare for Developers: DNS to CDN course
Cloud NativeAugust 18, 20264 min read

Handling CORS in Workers: A Developer's Guide to Secure APIs

Learn how to manage CORS in Cloudflare Workers. Master preflight requests, restrict origins, and secure your API headers for production-grade cross-origin access.

CloudflareWorkersCORSAPISecurityHeaders
Close-up of a computer screen displaying HTML, CSS, and JavaScript code

Previously in this course, we performed a final production audit to ensure our infrastructure was performant and secure. Now that our backend is hardened, we need to address a common friction point for browser-based clients: Cross-Origin Resource Sharing, or CORS.

If you have ever tried to fetch data from an API hosted on a different domain than your frontend, you’ve likely encountered the dreaded "CORS error." This happens because browsers enforce a security sandbox that prevents web pages from making requests to a domain other than the one that served the page, unless the server explicitly allows it.

Understanding CORS from First Principles

Browsers use CORS to protect users from malicious sites that might try to access data on behalf of a logged-in user. When a browser detects a cross-origin request, it evaluates the response headers from your Worker. If the correct headers aren't present, the browser blocks the response.

There are two types of requests in this flow:

  1. Simple Requests: Standard GET or POST requests that don't trigger a preflight.
  2. Preflight Requests: For complex requests (like those with custom headers or application/json content types), the browser sends an OPTIONS request first. Your Worker must respond to this OPTIONS request with the allowed origins and methods before the actual request is sent.

Configuring CORS Headers in Workers

To handle CORS in a Worker, you must intercept the request and return the appropriate headers. We typically define a constant for these headers to keep our code DRY.

JAVASCRIPT
const corsHeaders = {
  "Access-Control-Allow-Origin": "https://your-frontend.com", // Restrict to your domain
  "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
  "Access-Control-Max-Age": "86400",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export default {
  async fetch(request, env, ctx) {
    // 1. Handle Preflight (OPTIONS) requests
    if (request.method === "OPTIONS") {
      return new Response(null, {
        headers: {
          ...corsHeaders,
          "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers") || "",
        },
      });
    }

    // 2. Handle actual requests
    const response = await fetch(request);
    const newResponse = new Response(response.body, response);
    
    // Append headers to the response
    Object.keys(corsHeaders).forEach((key) => {
      newResponse.headers.set(key, corsHeaders[key]);
    });

    return newResponse;
  },
};

The Preflight Logic

The OPTIONS request acts as a "permission check." If you don't respond to this correctly, the browser will never send your POST or PUT request. Note how in the example above, we dynamically return the headers requested by the browser during the preflight phase.

HeaderPurpose
Access-Control-Allow-OriginSpecifies which domains can access your resources.
Access-Control-Allow-MethodsLists HTTP verbs allowed for the endpoint.
Access-Control-Allow-HeadersLists custom headers allowed during the request.
Access-Control-Max-AgeTells the browser how long to cache the preflight result.

Hands-on Exercise: Restricting Origins

Your current task is to modify your existing Worker to restrict access.

  1. Identify your production frontend domain (e.g., https://myapp.pages.dev).
  2. Update your corsHeaders object to use that specific domain instead of a wildcard (*).
  3. Deploy your Worker and use the browser's Network tab to confirm that the Access-Control-Allow-Origin header matches your domain exactly.

Common Pitfalls

  • Using * in Production: Never use Access-Control-Allow-Origin: * if your API handles user credentials (cookies or Authorization headers). Browsers will reject the request if the origin is a wildcard when credentials are required.
  • Forgetting OPTIONS: If your API works in Postman but fails in the browser, you are likely missing the OPTIONS handler.
  • Header Case Sensitivity: While HTTP headers are case-insensitive, it is best practice to follow standard naming conventions to avoid issues with older proxies or strict middleware.

FAQ

Q: Can I allow multiple origins? A: CORS only supports a single origin in the Access-Control-Allow-Origin header. If you need multiple, you must check the Origin header of the incoming request and dynamically set the Access-Control-Allow-Origin header to match if it exists in your "allowed" list.

Q: Why do I need Access-Control-Max-Age? A: It prevents the browser from sending an OPTIONS request for every single API call, significantly improving performance by caching the preflight result.

Q: Is CORS a security feature? A: It is a browser-side policy. It prevents browser-based attacks, but it does not protect your API from direct access via tools like curl or Postman. You must still implement proper Authentication Fundamentals to secure your data.

Recap

We've moved beyond simple request handling by implementing a robust CORS strategy. By managing OPTIONS requests and strictly defining allowed origins, we’ve hardened our API against cross-site scripting risks while ensuring our frontend can communicate securely with our Workers. As we learned in our Dynamic Header Manipulation lesson, controlling response headers is a critical skill for any platform engineer.

Up next: We will begin working with KV Storage to implement fast, distributed key-value data access.

Similar Posts