Created
September 12, 2025 14:35
-
-
Save schickling/1910629712787d87188c00f309035927 to your computer and use it in GitHub Desktop.
CORS Cloudflare Worker Proxy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-check | |
const corsHeaders = { | |
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS,DELETE', | |
'Access-Control-Max-Age': '86400', | |
} | |
const handleEvent = async (event) => { | |
const request = event.request | |
const origin = request.headers.get('Origin') | |
const url = new URL(request.url) | |
const targetUrl = url.searchParams.get('targetUrl') | |
console.log('targetUrl', targetUrl) | |
if (!targetUrl) { | |
return new Response('*', { status: 400, headers: { 'Content-type': 'text/plain' } }) | |
} | |
if (request.method === 'OPTIONS' && origin) { | |
const headers = { | |
...corsHeaders, | |
'Access-Control-Allow-Origin': origin, | |
'Access-Control-Allow-Headers': request.headers.get('access-control-request-headers'), | |
'Cross-Origin-Resource-Policy': 'cross-origin', | |
} | |
return new Response(null, { headers }) | |
} | |
let response | |
try { | |
const { readable, writable } = new TransformStream() | |
const originResponse = await fetch(targetUrl, { headers: request.headers, method: request.method }) | |
// Can be null e.g. for `HEAD` requests | |
if (originResponse.body === null) { | |
response = new Response('', originResponse) | |
} else { | |
originResponse.body.pipeTo(writable) | |
response = new Response(readable, originResponse) | |
} | |
// Here we are adding the `Retry-After` value to the `Access-Control-Expose-Headers` which some APIs (e.g. Spotify /artists endpoint) | |
// doesn't properly set when hitting a rate limit | |
if (originResponse.status === 429) { | |
const exposeHeadersVal = originResponse.headers.get('Access-Control-Expose-Headers') ?? '' | |
if (exposeHeadersVal.toLowerCase().includes('retry-after') === false) { | |
const newExposeHeadersVal = exposeHeadersVal === '' ? 'Retry-After' : `${exposeHeadersVal}, Retry-After` | |
response.headers.set('Access-Control-Expose-Headers', newExposeHeadersVal) | |
} | |
} | |
} catch (e) { | |
console.log(e) | |
const body = JSON.stringify({ error: e.toString() }, null, 2) | |
const headers = { 'Content-Type': 'application/json' } | |
response = new Response(body, { status: 500, headers }) | |
} | |
response.headers.set('Access-Control-Allow-Origin', origin ?? '*') | |
response.headers.set('Cross-Origin-Resource-Policy', 'cross-origin') | |
response.headers.append('Vary', 'Origin') | |
return response | |
} | |
addEventListener('fetch', (event) => { | |
// @ts-expect-error TODO fix CF types | |
event.passThroughOnException() | |
// @ts-expect-error TODO fix CF types | |
event.respondWith(handleEvent(event)) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment