mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Support trailing slashes
This commit is contained in:
parent
f33cb4cbc5
commit
f6bf39cf36
@ -5,8 +5,9 @@ import { parseCookieHeader, requestContextALS, type RequestContext } from "@/lib
|
||||
import { node } from "@elysiajs/node";
|
||||
import { getEnvVariable, getNodeEnvironment } from "@hexclave/shared/dist/utils/env";
|
||||
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 = {
|
||||
@ -39,6 +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 }) => withGlobalHeaders(handleUncaughtBackendError(error)))
|
||||
.get("/", () => htmlResponse(homeHtml()))
|
||||
.get("/dev-stats", () => htmlResponse(devStatsHtml()))
|
||||
.get("/health/error-handler-debug", () => htmlResponse(errorHandlerDebugHtml()))
|
||||
|
||||
31
apps/backend/src/server/error-handler.ts
Normal file
31
apps/backend/src/server/error-handler.ts
Normal 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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user