Skip to content

Instantly share code, notes, and snippets.

@schickling
Created September 12, 2025 14:35
Show Gist options
  • Save schickling/1910629712787d87188c00f309035927 to your computer and use it in GitHub Desktop.
Save schickling/1910629712787d87188c00f309035927 to your computer and use it in GitHub Desktop.
CORS Cloudflare Worker Proxy
// @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