From 0a72eff528dbe0dbf1db454089d95852d2827ce7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:58:26 +0000 Subject: [PATCH] Fix CORS caching issue on Cloudflare preview/staging deployments Resolves data feed failures when visiting PR preview URLs for the first time. The issue occurred because Cloudflare's edge cache was serving CORS responses without considering the requesting origin. Root Cause: Cloudflare's cache uses only the URL as the cache key by default. When different origins (pr-123, pr-184, etc.) request the same URL, they would receive the same cached response, including CORS headers meant for a different origin. This caused CORS errors when the browser rejected the mismatched Access-Control-Allow-Origin header. The Fix: Add Vary: Origin header to all CORS responses. This tells Cloudflare (and other caches) to include the Origin request header in the cache key, ensuring each origin gets its own cached response with correct CORS headers. Additional improvements: - Reduce CORS preflight cache to 10 seconds for staging/preview environments (down from 5 minutes) to allow quick recovery from any deployment issues - Production keeps 5 minute preflight cache for optimal performance This is the standard solution for caching origin-specific CORS responses. --- cloudflare-worker/worker.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/cloudflare-worker/worker.js b/cloudflare-worker/worker.js index 40841bb3..dc82a962 100644 --- a/cloudflare-worker/worker.js +++ b/cloudflare-worker/worker.js @@ -1,11 +1,16 @@ /** * Cloudflare Worker - CORS Proxy for Cambridge Beer Festival Data API - * + * * This worker proxies requests to data.cambridgebeerfestival.com and adds * the necessary CORS headers to allow the web app to access the data. - * + * * It also serves the festivals.json file which contains festival metadata * and enables dynamic loading of festival drinks. + * + * Cache Strategy: + * - Uses Vary: Origin header to ensure CORS responses are cached per origin + * - CORS preflight cache (Access-Control-Max-Age) is reduced to 10 seconds for + * staging/preview environments for quick recovery from deployment issues */ // Import festivals data directly - copied from data/festivals.json during build @@ -81,7 +86,7 @@ export default { // Clone the response and add CORS headers const newHeaders = new Headers(response.headers); setCorsHeaders(newHeaders, request); - + // Ensure JSON responses explicitly declare UTF-8 encoding // This prevents mojibake when non-ASCII characters (é, ö, ä, ñ) are present const contentType = newHeaders.get('Content-Type'); @@ -200,13 +205,28 @@ function parseDirectoryListingForBeverageTypes(html) { } function handleCorsPreflight(request) { + const origin = request.headers.get('Origin') || ''; + + // Set shorter CORS preflight cache for staging/preview environments + // to prevent stale CORS responses after deployments + let maxAge = '300'; // 5 minutes for production + + if (origin.endsWith('.staging-cambeerfestival.pages.dev') || + origin.endsWith('.cambeerfestival.pages.dev') || + origin === 'https://staging.cambeerfestival.app' || + origin.endsWith('.trycloudflare.com') || + origin.startsWith('http://localhost') || + origin.startsWith('http://127.0.0.1')) { + maxAge = '10'; // 10 seconds for development/staging + } + return new Response(null, { status: 204, headers: { ...getCorsHeaders(request), 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', - 'Access-Control-Max-Age': '300', // 5 minutes - allows quick recovery from CORS issues + 'Access-Control-Max-Age': maxAge, }, }); } @@ -219,6 +239,7 @@ function getCorsHeaders(request) { return { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': 'true', + 'Vary': 'Origin', // Tell caches to key by Origin header }; } @@ -231,6 +252,7 @@ function getCorsHeaders(request) { return { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': 'true', + 'Vary': 'Origin', // Tell caches to key by Origin header }; } @@ -241,6 +263,7 @@ function getCorsHeaders(request) { return { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': 'true', + 'Vary': 'Origin', // Tell caches to key by Origin header }; } @@ -252,6 +275,7 @@ function getCorsHeaders(request) { return { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': 'true', + 'Vary': 'Origin', // Tell caches to key by Origin header }; }