From 71ac719aee7d2bcb572b8e37b0ecf7587ea05406 Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Wed, 8 Apr 2026 02:07:20 +0800 Subject: [PATCH] fix: SPA fallback for all frontend routes, not just /home/* After migrating from Next.js to Vite SPA, routes like /auth/space/callback returned 404 because the static file server only had SPA fallback for /home/*. Now all non-API routes fall back to index.html for React Router to handle. --- src/langbot/pkg/api/http/controller/main.py | 31 ++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/langbot/pkg/api/http/controller/main.py b/src/langbot/pkg/api/http/controller/main.py index e0a78ea7..8e747777 100644 --- a/src/langbot/pkg/api/http/controller/main.py +++ b/src/langbot/pkg/api/http/controller/main.py @@ -105,23 +105,22 @@ class HTTPController: ): if os.path.exists(os.path.join(frontend_path, path + '.html')): path += '.html' - elif path.startswith('home/'): - # SPA fallback for /home/* sub-routes. - # Entity detail views use query params (e.g. /home/bots?id=uuid), - # so the pre-rendered list page is served directly via path + '.html'. - # This fallback handles any remaining unmatched sub-paths. - segments = path.rstrip('/').split('/') + elif not path.startswith('api/'): + # SPA fallback: serve index.html for all non-API, non-static routes + # so that React Router can handle client-side routing (Vite SPA). + # For /home/* sub-routes, first try parent .html files (pre-rendered pages). + if path.startswith('home/'): + segments = path.rstrip('/').split('/') + for i in range(len(segments) - 1, 0, -1): + parent_path = '/'.join(segments[:i]) + '.html' + if os.path.exists(os.path.join(frontend_path, parent_path)): + response = await quart.send_from_directory(frontend_path, parent_path, mimetype='text/html') + response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = '0' + return response - # Walk up parent segments looking for matching .html files - for i in range(len(segments) - 1, 0, -1): - parent_path = '/'.join(segments[:i]) + '.html' - if os.path.exists(os.path.join(frontend_path, parent_path)): - response = await quart.send_from_directory(frontend_path, parent_path, mimetype='text/html') - response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' - response.headers['Pragma'] = 'no-cache' - response.headers['Expires'] = '0' - return response - # Final fallback to index.html for /home/* routes + # Fallback to index.html for SPA client-side routing response = await quart.send_from_directory(frontend_path, 'index.html', mimetype='text/html') response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' response.headers['Pragma'] = 'no-cache'