Skip to content

Commit 51134ff

Browse files
Bekacrubytaesu
authored andcommitted
fix(router): only strip basePath when it is a leading prefix (#149)
* fix(router): only strip basePath when it is a leading prefix The router derived the route path by stripping basePath wherever it appeared via pathname.split(basePath), which dropped any segments before the first occurrence. A pathname like /x/api/test would then resolve onto the internal route /test even though it is not mounted under basePath. Strip basePath only when it is a leading, /-boundary prefix of the request pathname; pathnames outside basePath now resolve to a 404. Trailing slashes on basePath are normalized so /api/auth/ and /api/auth behave the same. * 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. --------- Co-authored-by: Taesu <bytaesu@gmail.com> (cherry picked from commit de59505)
1 parent 62db99c commit 51134ff

2 files changed

Lines changed: 105 additions & 20 deletions

File tree

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,85 @@ describe("base path", () => {
11241124
const text = await response.text();
11251125
expect(text).toBe("hello world");
11261126
});
1127+
1128+
it("should not route a path where the base path is not a leading prefix", async () => {
1129+
const endpoint = createEndpoint(
1130+
"/test",
1131+
{
1132+
method: "GET",
1133+
},
1134+
async () => {
1135+
return "hello world";
1136+
},
1137+
);
1138+
const router = createRouter({ endpoint }, { basePath: "/api" });
1139+
// The base path appears mid-path, not as a leading prefix, so it should
1140+
// not be stripped and the request should not resolve onto "/test".
1141+
const response = await router.handler(
1142+
new Request("http://localhost/x/api/test"),
1143+
);
1144+
expect(response.status).toBe(404);
1145+
});
1146+
1147+
it("should not invoke a handler when the base path appears after another segment", async () => {
1148+
let called = false;
1149+
const endpoint = createEndpoint(
1150+
"/test",
1151+
{
1152+
method: "GET",
1153+
},
1154+
async () => {
1155+
called = true;
1156+
return "hello world";
1157+
},
1158+
);
1159+
const router = createRouter({ endpoint }, { basePath: "/api" });
1160+
const response = await router.handler(
1161+
new Request("http://localhost/foo/api/test"),
1162+
);
1163+
expect(response.status).toBe(404);
1164+
expect(called).toBe(false);
1165+
});
1166+
1167+
it("should normalize a base path with a trailing slash", async () => {
1168+
const endpoint = createEndpoint(
1169+
"/test",
1170+
{
1171+
method: "GET",
1172+
},
1173+
async () => {
1174+
return "hello world";
1175+
},
1176+
);
1177+
const router = createRouter({ endpoint }, { basePath: "/api/" });
1178+
const response = await router.handler(
1179+
new Request("http://localhost/api/test"),
1180+
);
1181+
expect(response.status).toBe(200);
1182+
expect(await response.text()).toBe("hello world");
1183+
});
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: "/admin" });
1198+
// "/admin" is a leading substring of "/administrator" but not a
1199+
// "/"-boundary prefix, so the request must not resolve onto "/test".
1200+
const response = await router.handler(
1201+
new Request("http://localhost/administrator/test"),
1202+
);
1203+
expect(response.status).toBe(404);
1204+
expect(called).toBe(false);
1205+
});
11271206
});
11281207

11291208
/**

packages/better-call/src/router.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -157,31 +157,37 @@ 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;
163-
const path =
164-
config?.basePath && config.basePath !== "/"
165-
? pathname
166-
.split(config.basePath)
167-
.reduce((acc, curr, index) => {
168-
if (index !== 0) {
169-
if (index > 1) {
170-
acc.push(`${config.basePath}${curr}`);
171-
} else {
172-
acc.push(curr);
173-
}
174-
}
175-
return acc;
176-
}, [] as string[])
177-
.join("")
178-
: url.pathname;
179-
if (!path?.length) {
180-
return new Response(null, { status: 404, statusText: "Not Found" });
172+
// Strip `basePath` only when it is a leading, "/"-boundary prefix of the
173+
// request pathname. A pathname that does not start with the configured
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+
// path where basePath is only a leading substring, not a full segment.
177+
// The previous implementation stripped basePath wherever it occurred
178+
// (`pathname.split(basePath)`).
179+
let path: string;
180+
if (basePath) {
181+
if (!pathname.startsWith(`${basePath}/`)) {
182+
return new Response(null, { status: 404, statusText: "Not Found" });
183+
}
184+
path = pathname.slice(basePath.length);
185+
} else {
186+
path = pathname;
181187
}
182188

183-
// Reject paths with consecutive slashes
184-
if (/\/{2,}/.test(path)) {
189+
// Reject empty paths and paths with consecutive slashes.
190+
if (path.length === 0 || /\/{2,}/.test(path)) {
185191
return new Response(null, { status: 404, statusText: "Not Found" });
186192
}
187193

0 commit comments

Comments
 (0)