Compare commits

...
7 Commits
Author SHA1 Message Date
cupcakearmy f6ea6376e1 Merge pull request #221 from cupcakearmy/fix/spa-fallback-status-code
Fix/spa fallback status code
2026-09-26 13:33:33 +02:00
cupcakearmy 19e7899309 docs: add changelog note for SPA fallback + api 404 fix 2026-09-21 21:57:48 +02:00
cupcakearmy 9e69730bfd fix: keep SPA fallback for non-api paths but 404 unmatched /api routes 2026-09-21 21:39:09 +02:00
cupcakearmy b5e1c4b42a chore: cleanup 2026-09-21 21:27:50 +02:00
cupcakearmy 7660dd7c30 remove uncessary commens 2026-09-21 21:18:59 +02:00
cupcakearmy 685784c360 Merge branch 'main' into fix/spa-fallback-status-code 2026-09-21 21:16:57 +02:00
Matheus Leal 8034b1a501 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/<id>` 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/<id>`.

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
2026-09-03 16:09:40 -03:00
3 changed files with 28 additions and 3 deletions
+4
View File
@@ -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/<id>`, while unmatched `/api/*` paths still return 404.
### Breaking changes
- Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`.
+9 -3
View File
@@ -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,11 +43,13 @@ 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 =
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)
.merge(health_routes)
@@ -66,4 +72,4 @@ async fn main() {
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
}
}
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
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);
});
});