Makuhari Development Corporation
6 min read, 1107 words, last updated: 2026/1/12
TwitterLinkedInFacebookEmail

Debugging R2 CORS Configuration: When Workers Don't Work as Expected

CORS (Cross-Origin Resource Sharing) issues can be particularly frustrating when working with Cloudflare R2 and custom domains. Recently, I encountered a scenario where a developer was trying to enable CORS for R2 bucket access through their own domain, only to discover that their Worker wasn't executing at all. This debugging journey reveals several critical insights about Cloudflare's service hierarchy and domain ownership.

The Problem

The developer needed to serve CSV files from an R2 bucket through their custom domain (data.company.com) with CORS support for localhost development. They initially tried configuring CORS through the R2 Bucket UI, but this approach wasn't working for their architecture.

The key insight emerged early: R2 Bucket CORS configuration is designed for direct access to R2 endpoints, not for custom domains proxied through Cloudflare Zones.

When files are served through a custom domain like https://data.company.com/file.csv, the browser only sees the custom domain response headers. The R2 CORS configuration becomes invisible in this setup.

Investigation: Worker Not Executing

After understanding that CORS needed to be handled at the Zone level through a Worker, the developer implemented a CORS-enabled Worker. However, they quickly discovered that the Worker wasn't executing at all - a classic case of "Worker written ≠ Worker running."

Step 1: Verifying Worker Execution

The first debugging step was to create a minimal test Worker:

export default {
  async fetch(request, env) {
    return new Response('WORKER HIT', { status: 200 })
  }
}

When accessing the CSV file URL still returned the original content instead of "WORKER HIT", it confirmed that the Worker wasn't being triggered.

Step 2: Checking Worker Routes

The investigation revealed a common configuration issue: Worker Routes weren't properly configured. The correct route pattern should be:

data.company.com/*

Not:

  • company.com/* (too broad)
  • data.company.com (missing wildcard)
  • /api/* (too specific)

However, even with correct routes, the Worker still wasn't executing.

Root Cause: Domain Binding Conflicts

The real breakthrough came when examining the DNS records. The investigation revealed that data.company.com was showing up as a Type: R2 record, not a standard DNS record. This indicated that the subdomain was bound as an R2 Custom Domain, creating a fundamental conflict.

Understanding Cloudflare's Service Hierarchy

Cloudflare has a strict hierarchy for domain ownership within a Zone:

R2 Custom Domain
  > Pages Custom Domain
    > Worker Custom Domain
      > Standard DNS Records

When a domain is bound as an R2 Custom Domain for public bucket access, it creates an exclusive lock. Workers cannot override or intercept requests to that domain because R2 has higher priority in Cloudflare's routing system.

The DNS Evidence

The DNS panel showed:

Type: R2
Name: data.company.com
Content: public-datasets

This wasn't a regular CNAME or A record - it was an R2-specific domain binding that gave R2 exclusive control over that subdomain.

Solution: Reconfiguring Domain Architecture

Step 1: Unbind R2 Custom Domain

The only way to resolve this conflict was to remove the R2 Custom Domain binding:

  1. Navigate to R2 → Buckets → public-datasets
  2. Find the Custom Domains section
  3. Remove data.company.com from the R2 binding

This step cannot be performed from the DNS panel - it must be done from within the R2 service configuration.

Step 2: Implement Worker-Based Architecture

After unbinding the R2 domain, the correct architecture becomes:

Browser
  ↓
https://data.company.com/file.csv
  ↓
Cloudflare Worker (handles CORS)
  ↓
R2 Bucket (via env binding)

Step 3: Worker Implementation with CORS

Here's the production-ready Worker code:

const corsHeaders = {
  'Access-Control-Allow-Origin': 'http://localhost:8080',
  'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type',
  'Access-Control-Max-Age': '86400',
}
 
export default {
  async fetch(request, env) {
    // Handle preflight requests
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: corsHeaders,
      })
    }
 
    // Extract file path from URL
    const url = new URL(request.url)
    const key = url.pathname.slice(1) // Remove leading slash
 
    // Fetch from R2
    const object = await env.MY_BUCKET.get(key)
    
    if (!object) {
      return new Response('Not found', { status: 404 })
    }
 
    // Return with CORS headers
    return new Response(object.body, {
      headers: {
        ...corsHeaders,
        'Content-Type': 'text/csv',
        'Cache-Control': 'public, max-age=3600',
      },
    })
  },
}

Step 4: Configure Worker Custom Domain

After unbinding R2, the Worker Custom Domain configuration succeeds:

  1. Navigate to Workers & Pages → [Worker Name] → Settings → Domains & Routes
  2. Add Custom Domain: data.company.com
  3. The domain binding should complete without conflicts

Performance and Cost Considerations

DNS Propagation

Unlike traditional DNS changes, this reconfiguration doesn't require waiting for global DNS propagation. Cloudflare's internal service bindings typically take effect within 30-60 seconds.

Performance Impact

The Worker-based approach maintains performance characteristics:

  • Requests still hit Cloudflare edge locations
  • Caching remains effective
  • Response times are comparable to direct R2 access

Cost Implications

The primary cost consideration is Worker requests vs. R2 bandwidth:

  • Worker requests: Charged per invocation
  • R2 bandwidth: Free egress within Cloudflare
  • Trade-off: Slight increase in compute cost for significantly better control

For most use cases, the cost increase is minimal compared to the operational benefits of proper CORS handling and request control.

Lessons Learned

1. Understand Service Hierarchy

Cloudflare services have strict hierarchical relationships. When debugging routing issues, always check if multiple services are competing for the same domain.

2. CORS Responsibility Layers

CORS headers must be injected at the final response layer that the browser sees. In custom domain scenarios, this is never the origin service (R2) but always the edge service (Worker/Zone).

3. Domain Ownership is Exclusive

Within a Cloudflare Zone, each subdomain can only be "owned" by one service at a time. R2 Custom Domains, Pages, and Workers cannot share the same domain.

4. Always Verify Worker Execution

Before debugging Worker code, always verify the Worker is actually receiving requests. A simple string response test can save hours of CORS configuration troubleshooting.

5. DNS Panel Shows All Bindings

The DNS panel reveals all types of domain bindings, not just traditional records. Look for unusual "Type" values that indicate service-specific bindings.

Prevention Tips

  1. Choose architecture early: Decide between R2 public domains or Worker-based access before configuring custom domains
  2. Use consistent debugging patterns: Always test Worker execution before debugging application logic
  3. Monitor service conflicts: Regularly audit domain bindings across different Cloudflare services
  4. Document domain ownership: Keep track of which services control which subdomains in complex setups

This debugging experience highlights how modern cloud platforms abstract complexity while introducing new failure modes. Understanding the service hierarchy and domain ownership model is crucial for effective troubleshooting in serverless architectures.

Makuhari Development Corporation
法人番号: 6040001134259
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.