From 8034b1a5014d65f111b8323b184a29287699bb3b Mon Sep 17 00:00:00 2001 From: Matheus Leal Date: Thu, 3 Sep 2026 16:05:02 -0300 Subject: [PATCH 1/5] 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) + }) +}) From 7660dd7c30f8d36ef79239c47667b5fff27677c5 Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Mon, 21 Sep 2026 21:18:59 +0200 Subject: [PATCH 2/5] remove uncessary commens --- packages/backend/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs index df19ec0..a2cabc4 100644 --- a/packages/backend/src/main.rs +++ b/packages/backend/src/main.rs @@ -42,9 +42,6 @@ async fn main() { let api_routes = Router::new().nest("/v3", v3_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()).fallback(ServeFile::new(index)); let app = Router::new() @@ -69,4 +66,4 @@ async fn main() { axum::serve(listener, ServiceExt::::into_make_service(app)) .await .unwrap(); -} \ No newline at end of file +} From b5e1c4b42aa879a0fde3bd7094f44096f195325c Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Mon, 21 Sep 2026 21:27:50 +0200 Subject: [PATCH 3/5] chore: cleanup --- test/web/spa-fallback.spec.ts | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/test/web/spa-fallback.spec.ts b/test/web/spa-fallback.spec.ts index 012cbd1..28a2c51 100644 --- a/test/web/spa-fallback.spec.ts +++ b/test/web/spa-fallback.spec.ts @@ -1,19 +1,15 @@ -import { expect, test } from '@playwright/test' +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.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) - }) + 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) - }) -}) + 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); + }); +}); From 9e69730bfd2cef3081008b576570aee683c85e4f Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Mon, 21 Sep 2026 21:39:09 +0200 Subject: [PATCH 4/5] fix: keep SPA fallback for non-api paths but 404 unmatched /api routes --- packages/backend/src/main.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs index a2cabc4..bbab6ec 100644 --- a/packages/backend/src/main.rs +++ b/packages/backend/src/main.rs @@ -20,6 +20,10 @@ mod note; mod status; mod store; +async fn api_not_found() -> axum::http::StatusCode { + axum::http::StatusCode::NOT_FOUND +} + #[tokio::main] async fn main() { dotenv().ok(); @@ -39,7 +43,9 @@ async fn main() { .nest("/notes", notes_routes) .merge(status_routes); - let api_routes = Router::new().nest("/v3", v3_routes); + let api_routes = Router::new() + .nest("/v3", v3_routes) + .fallback(api_not_found); let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); let serve_dir = From 19e78993090253d61d644c0e134022bb09ac7c10 Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Mon, 21 Sep 2026 21:57:48 +0200 Subject: [PATCH 5/5] docs: add changelog note for SPA fallback + api 404 fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443dcc3..a249395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Frontend uses `@cryptgeon/shared` for crypto + payload codec (replacing the previous local `cryptgeon/shared`); heavy pack/unpack runs in a web worker. - CLI rebuilt with `vite-plus` (bundles all deps) and imports from `@cryptgeon/shared`. +### Fixed + +- SPA fallback now serves the app (200) for client-side routes such as `/about` and `/note/`, while unmatched `/api/*` paths still return 404. + ### Breaking changes - Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`.