Skip to content

Commit 45ec304

Browse files
committed
refactor(router): derive base path once and flatten path resolution
Compute the trailing-slash-normalized base path once at router creation instead of on every request, and resolve the request path with guard-clause early returns. Behavior is unchanged. Add a regression test for a sibling prefix that matches without a "/" boundary.
1 parent 691e092 commit 45ec304

2 files changed

Lines changed: 43 additions & 21 deletions

File tree

packages/better-call/src/router.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,28 @@ describe("base path", () => {
11811181
expect(response.status).toBe(200);
11821182
expect(await response.text()).toBe("hello world");
11831183
});
1184+
1185+
it("should not route when the base path matches without a '/' boundary", async () => {
1186+
let called = false;
1187+
const endpoint = createEndpoint(
1188+
"/test",
1189+
{
1190+
method: "GET",
1191+
},
1192+
async () => {
1193+
called = true;
1194+
return "hello world";
1195+
},
1196+
);
1197+
const router = createRouter({ endpoint }, { basePath: "/api" });
1198+
// "/api" is a leading substring of "/apifoo" but not a "/"-boundary
1199+
// prefix, so the request must not resolve onto "/test".
1200+
const response = await router.handler(
1201+
new Request("http://localhost/apifoo/test"),
1202+
);
1203+
expect(response.status).toBe(404);
1204+
expect(called).toBe(false);
1205+
});
11841206
});
11851207

11861208
/**

packages/better-call/src/router.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -157,36 +157,36 @@ export const createRouter = <
157157
}
158158
}
159159

160+
// Normalize the configured base path once. `basePath` is configuration, not
161+
// per-request input, so trailing-slash normalization belongs here rather than
162+
// in the request hot path. An empty result (unset, "/", or all slashes) means
163+
// "no base path": route on the full pathname.
164+
const basePath =
165+
config?.basePath && config.basePath !== "/"
166+
? config.basePath.replace(/\/+$/, "")
167+
: "";
168+
160169
const processRequest = async (request: Request) => {
161170
const url = new URL(request.url);
162171
const pathname = url.pathname;
163172
// Strip `basePath` only when it is a leading, "/"-boundary prefix of the
164173
// request pathname. A pathname that does not start with the configured
165-
// basePath is outside this router and resolves to a 404. The previous
166-
// implementation stripped basePath wherever it occurred
167-
// (`pathname.split(basePath)`), which let a path like "/x/api/test"
168-
// resolve onto the internal route "/test" even though it is not mounted
169-
// under basePath.
170-
let path: string | null;
171-
if (config?.basePath && config.basePath !== "/") {
172-
// Normalize trailing slashes so "/api/auth/" and "/api/auth" behave the same.
173-
const normalizedBasePath = config.basePath.replace(/\/+$/, "");
174-
if (!normalizedBasePath) {
175-
path = pathname;
176-
} else if (pathname.startsWith(`${normalizedBasePath}/`)) {
177-
path = pathname.slice(normalizedBasePath.length);
178-
} else {
179-
path = null;
174+
// basePath is outside this router and resolves to a 404, so a path like
175+
// "/x/api/test" never reaches "/test". The "/" boundary also rejects a
176+
// sibling prefix such as "/api/authevil/...". The previous implementation
177+
// stripped basePath wherever it occurred (`pathname.split(basePath)`).
178+
let path: string;
179+
if (basePath) {
180+
if (!pathname.startsWith(`${basePath}/`)) {
181+
return new Response(null, { status: 404, statusText: "Not Found" });
180182
}
183+
path = pathname.slice(basePath.length);
181184
} else {
182-
path = url.pathname;
183-
}
184-
if (path === null || path.length === 0) {
185-
return new Response(null, { status: 404, statusText: "Not Found" });
185+
path = pathname;
186186
}
187187

188-
// Reject paths with consecutive slashes
189-
if (/\/{2,}/.test(path)) {
188+
// Reject empty paths and paths with consecutive slashes.
189+
if (path.length === 0 || /\/{2,}/.test(path)) {
190190
return new Response(null, { status: 404, statusText: "Not Found" });
191191
}
192192

0 commit comments

Comments
 (0)