fix: return proper HTML for SPA routes instead of Bun error page

When accessing client-side routes (like /qsos) via curl or non-JS clients,
the server attempted to open them as static files, causing Bun to throw
an unhandled ENOENT error that showed an ugly error page.

Now checks if a path has a file extension before attempting to serve it.
Paths without extensions are immediately served index.html for SPA routing.
Also improves the 503 error page with user-friendly HTML when frontend build
is missing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-18 07:17:28 +01:00
parent 223461f536
commit 720144627e

View File

@@ -680,13 +680,28 @@ const app = new Elysia()
try { try {
const fullPath = `src/frontend/build${filePath}`; const fullPath = `src/frontend/build${filePath}`;
// Use Bun.file() which doesn't throw for non-existent files // For paths without extensions or directories, use SPA fallback immediately
// This prevents errors when trying to open directories as files
const ext = filePath.split('.').pop();
const hasExtension = ext !== filePath && ext.length <= 5; // Simple check for file extension
if (!hasExtension) {
// No extension means it's a route, not a file - serve index.html
const indexFile = Bun.file('src/frontend/build/index.html');
return new Response(indexFile, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
}
// Try to serve actual files (with extensions)
const file = Bun.file(fullPath); const file = Bun.file(fullPath);
const exists = file.exists(); const exists = file.exists();
if (exists) { if (exists) {
// Determine content type // Determine content type
const ext = filePath.split('.').pop();
const contentTypes = { const contentTypes = {
'js': 'application/javascript', 'js': 'application/javascript',
'css': 'text/css', 'css': 'text/css',
@@ -719,6 +734,7 @@ const app = new Elysia()
} }
} catch (err) { } catch (err) {
// File not found or error, fall through to SPA fallback // File not found or error, fall through to SPA fallback
logger.debug('Error serving static file, falling back to SPA', { path: pathname, error: err.message });
} }
// SPA fallback - serve index.html for all other routes // SPA fallback - serve index.html for all other routes
@@ -730,8 +746,16 @@ const app = new Elysia()
'Cache-Control': 'no-cache, no-store, must-revalidate', 'Cache-Control': 'no-cache, no-store, must-revalidate',
}, },
}); });
} catch { } catch (err) {
return new Response('Frontend not built. Run `bun run build`', { status: 503 }); logger.error('Frontend build not found', { error: err.message });
return new Response(
'<!DOCTYPE html><html><head><title>Quickawards - Unavailable</title></head>' +
'<body style="font-family: system-ui; max-width: 600px; margin: 100px auto; padding: 20px;">' +
'<h1>Service Temporarily Unavailable</h1>' +
'<p>The frontend application is not currently available. This usually means the application is being updated or restarted.</p>' +
'<p>Please try refreshing the page in a few moments.</p></body></html>',
{ status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
);
} }
}) })