From 8552254dbff15260a26938e4282b1e375700e8f5 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 10 Sep 2025 16:58:52 -0600 Subject: [PATCH] Update redirect for 307 --- apps/web/BROWSER_CACHING_FIX.md | 118 ++++++++++++++++ apps/web/CLOUDFLARE_REDIRECT_FIX.md | 132 ------------------ apps/web/src/middleware/global-security.ts | 1 - apps/web/src/routes/app.tsx | 8 +- .../datasets.$datasetId.permissions.index.tsx | 2 +- .../routes/app/_app/datasets.$datasetId.tsx | 2 +- .../src/routes/app/_settings/_permissions.tsx | 2 +- ...s.dataset-groups.$datasetGroupId.index.tsx | 2 +- .../_restricted_layout/_admin_only.tsx | 2 +- .../routes/app/_settings/settings.index.tsx | 2 +- apps/web/src/routes/app/index.tsx | 2 +- apps/web/src/routes/auth.logout.tsx | 2 +- apps/web/src/routes/index.tsx | 20 +-- 13 files changed, 137 insertions(+), 158 deletions(-) create mode 100644 apps/web/BROWSER_CACHING_FIX.md delete mode 100644 apps/web/CLOUDFLARE_REDIRECT_FIX.md diff --git a/apps/web/BROWSER_CACHING_FIX.md b/apps/web/BROWSER_CACHING_FIX.md new file mode 100644 index 000000000..ec03e5c31 --- /dev/null +++ b/apps/web/BROWSER_CACHING_FIX.md @@ -0,0 +1,118 @@ +# Browser Redirect Caching Issue - Fix Documentation + +## โœ… The Real Problem: Browser Caching + +Your issue wasn't with Cloudflare Workers or SSR - it was **browser caching of redirects**! + +### Evidence: +- โœ… **Incognito mode works perfectly** (no cache) +- โŒ **Regular browser fails** (has cached redirects) +- โŒ **After deployment, old cached redirects persist** + +## ๐Ÿ” Root Cause + +1. **301 (Permanent) redirects are cached aggressively** by browsers +2. Browsers remember these redirects **even after deployments** +3. The cached redirects were causing the loading issues + +## โœ… Solutions Applied + +### 1. Changed All Status Codes from 301 to 307 +**Why 307?** +- **301**: Permanent redirect (heavily cached) +- **302**: Temporary redirect (sometimes cached) +- **307**: Temporary redirect (NEVER cached) + +**Files Updated:** +- `/src/routes/app/index.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app.tsx` - Changed 302 โ†’ 307 +- `/src/routes/auth.logout.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app/_settings/settings.index.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app/_app/datasets.$datasetId.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app/_app/datasets.$datasetId.permissions.index.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app/_settings/_permissions/settings.dataset-groups.$datasetGroupId.index.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app/_settings/_restricted_layout/_admin_only.tsx` - Changed 301 โ†’ 307 +- `/src/routes/app/_settings/_permissions.tsx` - Changed 301 โ†’ 307 + +### 2. Added Cache Control Headers +Modified `/src/middleware/global-security.ts` to add no-cache headers for redirect routes: +```typescript +if (isRedirectRoute) { + headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'; + headers['Pragma'] = 'no-cache'; + headers['Expires'] = '0'; +} +``` + +### 3. Meta Refresh with Cache Prevention +Updated `/src/routes/index.tsx` with cache prevention meta tags: +```typescript +meta: [ + { 'http-equiv': 'refresh', content: '0; url=/app/home' }, + { 'http-equiv': 'Cache-Control', content: 'no-cache, no-store, must-revalidate' }, + { 'http-equiv': 'Pragma', content: 'no-cache' }, + { 'http-equiv': 'Expires', content: '0' }, +] +``` + +## ๐Ÿงน Clear Your Browser Cache + +**For existing users who might have cached 301 redirects:** + +### Chrome/Edge: +1. Open DevTools (F12) +2. Right-click the refresh button +3. Select "Empty Cache and Hard Reload" + +### Or Clear Specific Site Data: +1. Open DevTools โ†’ Application tab +2. Storage โ†’ Clear storage +3. Click "Clear site data" + +### Firefox: +1. Ctrl+Shift+Delete +2. Select "Cache" only +3. Clear for "Last hour" + +## ๐Ÿ“Š Status Code Reference + +| Code | Type | Cached? | Use Case | +|------|------|---------|----------| +| 301 | Permanent | โœ… Aggressively | Never for app redirects! | +| 302 | Temporary | โš ๏ธ Sometimes | Legacy, avoid | +| 303 | See Other | โŒ No | POST โ†’ GET redirect | +| 307 | Temporary | โŒ No | โœ… Best for app redirects | +| 308 | Permanent | โœ… Yes | Like 301 but preserves method | + +## ๐Ÿš€ Testing + +1. **Clear browser cache first** (important!) +2. **Build and deploy:** + ```bash + npm run build + npx wrangler deploy --env staging + ``` + +3. **Test scenarios:** + - Visit `/` โ†’ Should redirect to `/app/home` + - Visit `/app/` โ†’ Should redirect to `/app/home` + - Log out โ†’ Should redirect to `/auth/login` + - All should work on first visit (cold start) + +## ๐ŸŽฏ Key Takeaways + +1. **Never use 301 for application redirects** - they're meant for permanent URL changes +2. **Use 307 for temporary redirects** that shouldn't be cached +3. **Browser caching can persist across deployments** and cause mysterious issues +4. **Incognito mode is your friend** for testing caching issues + +## ๐Ÿ”ฎ Future Recommendations + +1. **Consider server-side redirects** in Cloudflare configuration for static redirects +2. **Monitor with Chrome DevTools Network tab** - check "Disable cache" when debugging +3. **Use `wrangler tail` to see if requests are even hitting your worker** + +## Related Issues +- [MDN: HTTP redirect status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages) +- [Chrome: Clear cache for specific site](https://support.google.com/chrome/answer/2392709) +- [Cloudflare: Page Rules for redirects](https://developers.cloudflare.com/rules/page-rules/) diff --git a/apps/web/CLOUDFLARE_REDIRECT_FIX.md b/apps/web/CLOUDFLARE_REDIRECT_FIX.md deleted file mode 100644 index 37eeaa5ed..000000000 --- a/apps/web/CLOUDFLARE_REDIRECT_FIX.md +++ /dev/null @@ -1,132 +0,0 @@ -# Cloudflare Workers Redirect Issue - Fix Documentation - -## Problem Summary -After deploying to Cloudflare Workers, the application fails to load when accessing redirect URLs (like `/` or `/app/`) during cold starts. However, direct URLs work fine, and once loaded, all redirects work correctly. - -## Root Cause -The issue stems from how Cloudflare Workers handles SSR (Server-Side Rendering) during cold starts: -1. JavaScript redirects thrown during SSR (`beforeLoad` hooks) don't execute properly -2. The SSR context on edge workers has limitations with redirect handling -3. Cookie-based authentication checks may not initialize properly during cold starts - -## Solutions Applied - -### Solution 1: Client-Only Redirects (Currently Implemented) -Modified routes to only perform redirects on the client-side: - -**Files Modified:** -- `/src/routes/index.tsx` -- `/src/routes/app/index.tsx` - -**Changes:** -```typescript -// Before (problematic) -beforeLoad: async () => { - throw redirect({ to: '/app/home', replace: true, statusCode: 302 }); -} - -// After (fixed) -beforeLoad: async () => { - if (!isServer) { - throw redirect({ to: '/app/home', replace: true }); - } -}, -loader: async () => { - if (isServer) { - return { shouldRedirect: true, redirectTo: '/app/home' }; - } - return {}; -}, -component: () => { - const data = Route.useLoaderData(); - React.useEffect(() => { - if (data?.shouldRedirect) { - window.location.href = data.redirectTo; - } - }, [data]); - return
Redirecting...
; -} -``` - -### Solution 2: Meta Refresh Fallback (Alternative) -If issues persist, use HTML meta refresh as a more reliable fallback: - -```typescript -head: () => ({ - meta: [ - { 'http-equiv': 'refresh', content: '0; url=/app/home' }, - ], -}) -``` - -See `/src/routes/index.alternative.tsx` for full implementation. - -## Additional Recommendations - -### 1. Optimize Cold Starts -Add warming strategy in `wrangler.jsonc`: -```json -{ - "triggers": { - "crons": ["*/5 * * * *"] - } -} -``` - -### 2. Consider Edge-Specific Routing -For production, consider using Cloudflare's native routing features: -```json -// wrangler.jsonc -{ - "routes": [ - { "pattern": "/", "redirect": "/app/home" } - ] -} -``` - -### 3. Monitor Performance -Enable Cloudflare Analytics to track cold start frequency: -```json -{ - "observability": { - "enabled": true, - "head_sampling_rate": 1 - } -} -``` - -## Testing the Fix - -1. **Local Testing:** - ```bash - npm run build - npx wrangler dev - ``` - -2. **Staging Deployment:** - ```bash - npx wrangler deploy --env staging - ``` - -3. **Test Scenarios:** - - Cold start: Open incognito window, navigate to `/` - - Warm start: Navigate to `/` after loading any other page - - Direct access: Navigate directly to `/app/home` - - Authentication flow: Test login/logout redirects - -## Rollback Plan -If issues persist after deployment: -1. Keep original files as `.backup` -2. Revert changes using git -3. Consider server-side HTTP redirects in Cloudflare configuration - -## Related Issues -- TanStack Start SSR limitations: https://tanstack.com/router/latest/docs/framework/react/start/ssr -- Cloudflare Workers SSR: https://developers.cloudflare.com/workers/examples/render-react-app/ -- Supabase SSR auth: https://supabase.com/docs/guides/auth/server-side-rendering - -## Contact -If problems persist, consider: -1. Opening issue in TanStack Start repository -2. Cloudflare Workers Discord community -3. Implementing pure HTTP redirects at edge level diff --git a/apps/web/src/middleware/global-security.ts b/apps/web/src/middleware/global-security.ts index 4f646a589..12ef8a6c3 100644 --- a/apps/web/src/middleware/global-security.ts +++ b/apps/web/src/middleware/global-security.ts @@ -9,7 +9,6 @@ export const securityMiddleware = createMiddleware({ type: 'function' }).server( const url = new URL(request.url); const isEmbed = url.pathname.startsWith('/embed/'); - // Set security headers BEFORE calling next() to ensure they're set only once setHeaders(createSecurityHeaders(isEmbed)); const result = await next(); diff --git a/apps/web/src/routes/app.tsx b/apps/web/src/routes/app.tsx index 205262e07..f91b1b6a5 100644 --- a/apps/web/src/routes/app.tsx +++ b/apps/web/src/routes/app.tsx @@ -21,7 +21,7 @@ export const Route = createFileRoute('/app')({ if (isExpired || !accessToken) { console.error('Access token is expired or not found'); - throw redirect({ to: '/auth/login', replace: true, statusCode: 302 }); + throw redirect({ to: '/auth/login', replace: true, statusCode: 307 }); } return { @@ -29,7 +29,7 @@ export const Route = createFileRoute('/app')({ }; } catch (error) { console.error('Error in app route beforeLoad:', error); - throw redirect({ to: '/auth/login', replace: true, statusCode: 302 }); + throw redirect({ to: '/auth/login', replace: true, statusCode: 307 }); } }, loader: async ({ context }) => { @@ -48,7 +48,7 @@ export const Route = createFileRoute('/app')({ if (!user) { console.error('User not found - redirecting to login'); - throw redirect({ to: '/auth/login', replace: true, statusCode: 302 }); + throw redirect({ to: '/auth/login', replace: true, statusCode: 307 }); } return { @@ -60,7 +60,7 @@ export const Route = createFileRoute('/app')({ }; } catch (error) { console.error('Error in app route loader:', error); - throw redirect({ to: '/auth/login', replace: true, statusCode: 302 }); + throw redirect({ to: '/auth/login', replace: true, statusCode: 307 }); } }, component: () => { diff --git a/apps/web/src/routes/app/_app/datasets.$datasetId.permissions.index.tsx b/apps/web/src/routes/app/_app/datasets.$datasetId.permissions.index.tsx index 753282378..48b4b6b92 100644 --- a/apps/web/src/routes/app/_app/datasets.$datasetId.permissions.index.tsx +++ b/apps/web/src/routes/app/_app/datasets.$datasetId.permissions.index.tsx @@ -6,7 +6,7 @@ export const Route = createFileRoute('/app/_app/datasets/$datasetId/permissions/ to: '/app/datasets/$datasetId/permissions/overview', params, replace: true, - statusCode: 301, + statusCode: 307, }); }, }); diff --git a/apps/web/src/routes/app/_app/datasets.$datasetId.tsx b/apps/web/src/routes/app/_app/datasets.$datasetId.tsx index 69feb17c5..a236bbe97 100644 --- a/apps/web/src/routes/app/_app/datasets.$datasetId.tsx +++ b/apps/web/src/routes/app/_app/datasets.$datasetId.tsx @@ -13,7 +13,7 @@ export const Route = createFileRoute('/app/_app/datasets/$datasetId')({ to: '/app/datasets/$datasetId/overview', params, replace: true, - statusCode: 301, + statusCode: 307, }); } }, diff --git a/apps/web/src/routes/app/_settings/_permissions.tsx b/apps/web/src/routes/app/_settings/_permissions.tsx index d73957ba1..ea1c829a6 100644 --- a/apps/web/src/routes/app/_settings/_permissions.tsx +++ b/apps/web/src/routes/app/_settings/_permissions.tsx @@ -9,7 +9,7 @@ export const Route = createFileRoute('/app/_settings/_permissions')({ const { queryClient } = context; const userData = await prefetchGetMyUserInfo(queryClient); if (!userData || !userData.organizations || !checkIfUserIsAdmin(userData.organizations[0])) { - throw redirect({ to: '/auth/login', replace: true, statusCode: 301 }); + throw redirect({ to: '/auth/login', replace: true, statusCode: 307 }); } }, }); diff --git a/apps/web/src/routes/app/_settings/_permissions/settings.dataset-groups.$datasetGroupId.index.tsx b/apps/web/src/routes/app/_settings/_permissions/settings.dataset-groups.$datasetGroupId.index.tsx index 8c8d043dc..ee6a1bfc5 100644 --- a/apps/web/src/routes/app/_settings/_permissions/settings.dataset-groups.$datasetGroupId.index.tsx +++ b/apps/web/src/routes/app/_settings/_permissions/settings.dataset-groups.$datasetGroupId.index.tsx @@ -8,7 +8,7 @@ export const Route = createFileRoute( to: '/app/settings/dataset-groups/$datasetGroupId/datasets', params, replace: true, - statusCode: 301, + statusCode: 307, }); }, }); diff --git a/apps/web/src/routes/app/_settings/_restricted_layout/_admin_only.tsx b/apps/web/src/routes/app/_settings/_restricted_layout/_admin_only.tsx index 29c2ecbe6..177440fa9 100644 --- a/apps/web/src/routes/app/_settings/_restricted_layout/_admin_only.tsx +++ b/apps/web/src/routes/app/_settings/_restricted_layout/_admin_only.tsx @@ -7,7 +7,7 @@ export const Route = createFileRoute('/app/_settings/_restricted_layout/_admin_o const { queryClient } = context; const userData = await prefetchGetMyUserInfo(queryClient); if (!userData || !userData.organizations || !checkIfUserIsAdmin(userData.organizations[0])) { - throw redirect({ to: '/auth/login', replace: true, statusCode: 301 }); + throw redirect({ to: '/auth/login', replace: true, statusCode: 307 }); } }, diff --git a/apps/web/src/routes/app/_settings/settings.index.tsx b/apps/web/src/routes/app/_settings/settings.index.tsx index 17f43d9d4..57bc65b38 100644 --- a/apps/web/src/routes/app/_settings/settings.index.tsx +++ b/apps/web/src/routes/app/_settings/settings.index.tsx @@ -5,7 +5,7 @@ export const Route = createFileRoute('/app/_settings/settings/')({ throw redirect({ to: '/app/settings/profile', replace: true, - statusCode: 301, + statusCode: 307, }); }, }); diff --git a/apps/web/src/routes/app/index.tsx b/apps/web/src/routes/app/index.tsx index d161ecb22..a5cf81e42 100644 --- a/apps/web/src/routes/app/index.tsx +++ b/apps/web/src/routes/app/index.tsx @@ -2,7 +2,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'; export const Route = createFileRoute('/app/')({ beforeLoad: async () => { - throw redirect({ to: '/app/home', replace: true, statusCode: 301 }); + throw redirect({ to: '/app/home', replace: true, statusCode: 307 }); }, component: () => null, }); diff --git a/apps/web/src/routes/auth.logout.tsx b/apps/web/src/routes/auth.logout.tsx index c6f448f20..734dc3fae 100644 --- a/apps/web/src/routes/auth.logout.tsx +++ b/apps/web/src/routes/auth.logout.tsx @@ -13,6 +13,6 @@ export const Route = createFileRoute('/auth/logout')({ preload: false, loader: async () => { await signOut(); - throw redirect({ to: '/auth/login', statusCode: 301 }); + throw redirect({ to: '/auth/login', statusCode: 307 }); }, }); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index cc8ef4c8e..aadfae0c1 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -4,7 +4,12 @@ import { isServer } from '@/lib/window'; export const Route = createFileRoute('/')({ head: () => ({ - meta: [{ 'http-equiv': 'refresh', content: '0; url=/app/home' }], + meta: [ + { 'http-equiv': 'refresh', content: '0; url=/app/home' }, + { 'http-equiv': 'Cache-Control', content: 'no-cache, no-store, must-revalidate' }, + { 'http-equiv': 'Pragma', content: 'no-cache' }, + { 'http-equiv': 'Expires', content: '0' }, + ], }), beforeLoad: async () => { // Only redirect on client-side to avoid SSR issues during cold starts @@ -19,16 +24,5 @@ export const Route = createFileRoute('/')({ } return {}; }, - component: () => { - const data = Route.useLoaderData(); - - // Client-side redirect after hydration - React.useEffect(() => { - if (data?.shouldRedirect) { - window.location.href = data.redirectTo; - } - }, [data]); - - return
Redirecting...
; - }, + component: () => null, });