Merge remote-tracking branch 'origin/dev' into codex/fix-elysia-production-compat

# Conflicts:
#	apps/backend/src/server/app.ts
This commit is contained in:
Bilal Godil 2026-07-13 12:40:08 -07:00
commit 473d8c198c
4 changed files with 68 additions and 29 deletions

View File

@ -4,10 +4,10 @@ import { NextNotFoundError } from "@/lib/runtime/navigation";
import { parseCookieHeader, requestContextALS, type RequestContext } from "@/lib/runtime/request-context";
import { node } from "@elysiajs/node";
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
import { captureError } from "@hexclave/shared/dist/utils/errors";
import { Elysia } from "elysia";
import { runRequestPipeline } from "./middleware";
import { createBackendRequest } from "./backend-request";
import { handleUncaughtBackendError } from "./error-handler";
import { runRequestPipeline } from "./middleware";
import { MalformedRouteParamError, matchRoute } from "./registry";
const globalSecurityHeaders = {
@ -40,13 +40,7 @@ export const app = new Elysia({
const pathname = new URL(request.url).pathname;
console.log(`[Elysia] ${request.method} ${pathname} ${set.status} ${elapsedMilliseconds}ms`);
})
.onError(({ error }) => {
// Smart route handlers sanitize their own errors. This is the final boundary
// for errors from raw routes and framework code, which Elysia would otherwise
// return to the client verbatim.
captureError("elysia-request-handler", error);
return internalServerErrorResponse();
})
.onError(({ error }) => withGlobalHeaders(handleUncaughtBackendError(error)))
.get("/", () => htmlResponse(homeHtml()))
.get("/dev-stats", () => htmlResponse(devStatsHtml()))
.get("/health/error-handler-debug", () => htmlResponse(errorHandlerDebugHtml()))
@ -169,23 +163,6 @@ function htmlResponse(body: string, status = 200) {
}));
}
function internalServerErrorResponse() {
return withGlobalHeaders(new Response("Internal Server Error", {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
}));
}
import.meta.vitest?.test("unhandled errors do not expose their message", async ({ expect }) => {
const response = internalServerErrorResponse();
expect(response.status).toBe(500);
expect(await response.text()).toBe("Internal Server Error");
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
});
import.meta.vitest?.test("API version migrations do not expose their internal rewrite", async ({ expect }) => {
const { vi } = import.meta.vitest!;
vi.stubEnv("HEXCLAVE_ARTIFICIAL_DEVELOPMENT_DELAY_MS", "0");

View File

@ -0,0 +1,31 @@
import { captureError } from "@hexclave/shared/dist/utils/errors";
function createUncaughtErrorResponse() {
return new Response("Internal Server Error", {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
});
}
export function handleUncaughtBackendError(error: unknown) {
captureError("backend-global-error", error);
return createUncaughtErrorResponse();
}
import.meta.vitest?.test("uncaught backend errors do not expose internal details", async ({ expect }) => {
const response = createUncaughtErrorResponse();
expect({
status: response.status,
contentType: response.headers.get("content-type"),
body: await response.text(),
}).toMatchInlineSnapshot(`
{
"body": "Internal Server Error",
"contentType": "text/plain; charset=utf-8",
"status": 500,
}
`);
});

View File

@ -103,9 +103,11 @@ function parseApiVersionStringToArray(version: string): [number, number] {
function matchPath(path: string, toMatchWith: string): Record<string, string | string[]> | false {
// get the relative part, and modify it to have a leading slash, without a trailing slash, without ./.., etc.
const url = new URL(path + "/", "http://example.com");
const modifiedPath = url.pathname.slice(1, -1);
// Normalize to relative path segments without a trailing slash. Appending "/" before URL parsing
// turns an already-trailing path into "//", which URL interprets as a protocol-relative URL during
// the final recursive match and rejects because it has no host.
const url = new URL(path === "" ? "/" : path, "http://example.com");
const modifiedPath = url.pathname.slice(1).replace(/\/+$/, "");
const modifiedToMatchWith = toMatchWith.slice(1);
if (modifiedPath === "" && modifiedToMatchWith === "") {
@ -146,6 +148,20 @@ function matchPath(path: string, toMatchWith: string): Record<string, string | s
}
}
import.meta.vitest?.test("matches routes with trailing slashes", ({ expect }) => {
expect({
staticRoute: SmartRouter.matchNormalizedPath("/api/v1/", "/api/v1"),
dynamicRoute: SmartRouter.matchNormalizedPath("/users/user-123/", "/users/[user_id]"),
}).toMatchInlineSnapshot(`
{
"dynamicRoute": {
"user_id": "user-123",
},
"staticRoute": {},
}
`);
});
/**
* Modified from: https://github.com/vercel/next.js/blob/6ff13369bb18045657d0f84ddc86b540340603a1/packages/next/src/shared/lib/router/utils/app-paths.ts#L23
*/

View File

@ -10,6 +10,21 @@ describe("without project ID", () => {
it("should load", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1");
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": deindent\`
Welcome to the Hexclave API endpoint! Please refer to the documentation at https://docs.hexclave.com.
Authentication: None
\`,
"headers": Headers { <some fields may have been hidden> },
}
`);
});
it("should load with a trailing slash", async ({ expect }) => {
const response = await niceBackendFetch("/api/v1/");
expect(response).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,