fix: add backoff and skip init for non-API routes
- commit
- 2d8d7a10c22c00252c960e7fc3f17dac8ae67f00
- author
- bnema · · 2026-01-18 21:23 UTC
- parent
- fe76e67
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +25 -5
M src/hooks.server.ts +25 -5
| @@ -35,19 +35,34 @@ async function setAllUsersOffline() { | |||
| 35 | 35 | ||
| 36 | 36 | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) | |
| 37 | 37 | let initPromise: Promise<void> | null = null; | |
| 38 | + | let lastFailedAttempt = 0; | |
| 39 | + | let failureCount = 0; | |
| 40 | + | const BASE_COOLDOWN = 5000; // 5 seconds | |
| 41 | + | const MAX_COOLDOWN = 60000; // 1 minute max | |
| 38 | 42 | ||
| 39 | 43 | function initializeServer(): Promise<void> { | |
| 40 | 44 | if (initPromise) return initPromise; | |
| 41 | 45 | ||
| 46 | + | // Exponential backoff after failures | |
| 47 | + | if (lastFailedAttempt > 0) { | |
| 48 | + | const cooldown = Math.min(BASE_COOLDOWN * Math.pow(2, failureCount - 1), MAX_COOLDOWN); | |
| 49 | + | if (Date.now() - lastFailedAttempt < cooldown) { | |
| 50 | + | return Promise.resolve(); | |
| 51 | + | } | |
| 52 | + | } | |
| 53 | + | ||
| 42 | 54 | initPromise = (async () => { | |
| 43 | 55 | try { | |
| 44 | 56 | log.info('Initializing server...'); | |
| 45 | 57 | await ensureDefaultChatRoom(); | |
| 46 | 58 | await setAllUsersOffline(); | |
| 47 | 59 | log.info('Server initialized successfully'); | |
| 60 | + | lastFailedAttempt = 0; | |
| 61 | + | failureCount = 0; | |
| 48 | 62 | } catch (error: unknown) { | |
| 49 | - | log.error('Failed to initialize server:', { error }); | |
| 50 | - | // Reset so next request can retry | |
| 63 | + | log.error('Failed to initialize server:', { error, failureCount: failureCount + 1 }); | |
| 64 | + | lastFailedAttempt = Date.now(); | |
| 65 | + | failureCount++; | |
| 51 | 66 | initPromise = null; | |
| 52 | 67 | } | |
| 53 | 68 | })(); | |
| @@ -56,10 +71,15 @@ function initializeServer(): Promise<void> { | |||
| 56 | 71 | } | |
| 57 | 72 | ||
| 58 | 73 | export const handle: Handle = async ({ event, resolve }) => { | |
| 59 | - | // Lazy initialization on first request (Cloudflare Workers compatible) | |
| 60 | - | await initializeServer(); | |
| 74 | + | const path = event.url.pathname; | |
| 75 | + | ||
| 76 | + | // Skip initialization for static assets and non-API routes | |
| 77 | + | const needsInit = path.startsWith('/api/') && !path.startsWith('/api/health'); | |
| 78 | + | if (needsInit) { | |
| 79 | + | await initializeServer(); | |
| 80 | + | } | |
| 61 | 81 | ||
| 62 | - | log.debug('Handling request', { path: event.url.pathname }); | |
| 82 | + | log.debug('Handling request', { path }); | |
| 63 | 83 | ||
| 64 | 84 | // CSRF protection: validate Origin header for state-changing requests | |
| 65 | 85 | if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { | |
| @@ -35,19 +35,34 @@ async function setAllUsersOffline() { | |||||
| 35 | 35 | ||||
| 36 | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) | 36 | // Initialize server (deferred, non-blocking for Cloudflare Workers compatibility) | ||
| 37 | let initPromise: Promise<void> | null = null; | 37 | let initPromise: Promise<void> | null = null; | ||
| 38 | + | let lastFailedAttempt = 0; | |||
| 39 | + | let failureCount = 0; | |||
| 40 | + | const BASE_COOLDOWN = 5000; // 5 seconds | |||
| 41 | + | const MAX_COOLDOWN = 60000; // 1 minute max | |||
| 38 | 42 | ||||
| 39 | function initializeServer(): Promise<void> { | 43 | function initializeServer(): Promise<void> { | ||
| 40 | if (initPromise) return initPromise; | 44 | if (initPromise) return initPromise; | ||
| 41 | 45 | ||||
| 46 | + | // Exponential backoff after failures | |||
| 47 | + | if (lastFailedAttempt > 0) { | |||
| 48 | + | const cooldown = Math.min(BASE_COOLDOWN * Math.pow(2, failureCount - 1), MAX_COOLDOWN); | |||
| 49 | + | if (Date.now() - lastFailedAttempt < cooldown) { | |||
| 50 | + | return Promise.resolve(); | |||
| 51 | + | } | |||
| 52 | + | } | |||
| 53 | + | ||||
| 42 | initPromise = (async () => { | 54 | initPromise = (async () => { | ||
| 43 | try { | 55 | try { | ||
| 44 | log.info('Initializing server...'); | 56 | log.info('Initializing server...'); | ||
| 45 | await ensureDefaultChatRoom(); | 57 | await ensureDefaultChatRoom(); | ||
| 46 | await setAllUsersOffline(); | 58 | await setAllUsersOffline(); | ||
| 47 | log.info('Server initialized successfully'); | 59 | log.info('Server initialized successfully'); | ||
| 60 | + | lastFailedAttempt = 0; | |||
| 61 | + | failureCount = 0; | |||
| 48 | } catch (error: unknown) { | 62 | } catch (error: unknown) { | ||
| 49 | - | log.error('Failed to initialize server:', { error }); | 63 | + | log.error('Failed to initialize server:', { error, failureCount: failureCount + 1 }); |
| 50 | - | // Reset so next request can retry | 64 | + | lastFailedAttempt = Date.now(); |
| 65 | + | failureCount++; | |||
| 51 | initPromise = null; | 66 | initPromise = null; | ||
| 52 | } | 67 | } | ||
| 53 | })(); | 68 | })(); | ||
| @@ -56,10 +71,15 @@ function initializeServer(): Promise<void> { | |||||
| 56 | } | 71 | } | ||
| 57 | 72 | ||||
| 58 | export const handle: Handle = async ({ event, resolve }) => { | 73 | export const handle: Handle = async ({ event, resolve }) => { | ||
| 59 | - | // Lazy initialization on first request (Cloudflare Workers compatible) | 74 | + | const path = event.url.pathname; |
| 60 | - | await initializeServer(); | 75 | + | |
| 76 | + | // Skip initialization for static assets and non-API routes | |||
| 77 | + | const needsInit = path.startsWith('/api/') && !path.startsWith('/api/health'); | |||
| 78 | + | if (needsInit) { | |||
| 79 | + | await initializeServer(); | |||
| 80 | + | } | |||
| 61 | 81 | ||||
| 62 | - | log.debug('Handling request', { path: event.url.pathname }); | 82 | + | log.debug('Handling request', { path }); |
| 63 | 83 | ||||
| 64 | // CSRF protection: validate Origin header for state-changing requests | 84 | // CSRF protection: validate Origin header for state-changing requests | ||
| 65 | if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { | 85 | if (event.request.method !== 'GET' && event.request.method !== 'HEAD') { | ||