From 8034b1a5014d65f111b8323b184a29287699bb3b Mon Sep 17 00:00:00 2001 From: Matheus Leal Date: Thu, 3 Sep 2026 16:05:02 -0300 Subject: [PATCH] fix: serve SPA fallback with 200 instead of 404 `ServeDir::not_found_service` wraps the fallback in `SetStatus`, which forces every response to `404 Not Found`. Client side routes such as `/note/` and `/about` were therefore served the correct `index.html` but with a 404 status. Use `ServeDir::fallback` instead, which leaves the status untouched. A note that genuinely does not exist is still reported as 404 by `/api/notes/`. Behind a reverse proxy this made effectively every document request show up as a 4xx, skewing error rate dashboards and triggering false alerts. Fixes #217 --- packages/backend/src/main.rs | 5 ++++- test/web/spa-fallback.spec.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 test/web/spa-fallback.spec.ts diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs index e738eea..3fd0071 100644 --- a/packages/backend/src/main.rs +++ b/packages/backend/src/main.rs @@ -51,8 +51,11 @@ async fn main() { .merge(status_routes); let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); + // SPA fallback: serve `index.html` for client side routes. + // `fallback` instead of `not_found_service`, as the latter forces a `404` status code, + // while the document itself is served successfully. A missing note is signalled by the API. let serve_dir = - ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index)); + ServeDir::new(config::FRONTEND_PATH.to_string()).fallback(ServeFile::new(index)); let app = Router::new() .nest("/api", api_routes) .fallback_service(serve_dir) diff --git a/test/web/spa-fallback.spec.ts b/test/web/spa-fallback.spec.ts new file mode 100644 index 0000000..012cbd1 --- /dev/null +++ b/test/web/spa-fallback.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test' + +// The SPA fallback serves index.html for client side routes. The document is +// served successfully, so it must not be reported as 404. +// A missing note is signalled by the API on /api/notes/ instead. +// https://github.com/cupcakearmy/cryptgeon/issues/217 +test.describe('@web', () => { + for (const path of ['/', '/about', '/note/does-not-exist']) { + test(`serves ${path} with status 200`, async ({ request }) => { + const response = await request.get(path) + expect(response.status()).toBe(200) + }) + } + + test('api still reports a missing note as 404', async ({ request }) => { + const response = await request.get('/api/notes/does-not-exist') + expect(response.status()).toBe(404) + }) +})