From 7eac0faf0e5d0f4d79d7b862b5a1a9ac9845c475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Mon, 7 Sep 2026 10:46:23 +0100 Subject: [PATCH] fix: support multi-segment base paths in use() app.use('/foo/bar', handler) never actually matched incoming requests because the request path was truncated down to just its first segment before being looked up against the (correctly, full-path-keyed) bwares and apps tables, so anything mounted on a base with more than one "folder" silently 404'd. Replaces that truncated lookup with a small helper that scans the registered base keys and picks the longest one that's a full-segment prefix of the request path, so both middleware bases and sub-app mounts work regardless of how many segments they have. Single-segment bases keep working exactly as before. --- packages/polka/index.js | 31 +++++++++++++++++----- tests/polka.js | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/polka/index.js b/packages/polka/index.js index 8df9d43..f84d5b4 100644 --- a/packages/polka/index.js +++ b/packages/polka/index.js @@ -17,6 +17,21 @@ function mutate(str, req) { req.path = req.path.substring(str.length) || '/'; } +// Finds the longest registered `key` (from `use(base, ...)`) that is a +// full-segment prefix of `path`, so bases with more than one "folder" +// (eg. `/foo/bar`) can be matched instead of only ever comparing the +// first segment of `path`. +function matchBase(path, keys) { + let base, blen=0, i=0, k, klen; + for (; i < keys.length; i++) { + k = keys[i]; klen = k.length; + if (klen > blen && path.length >= klen && path.substring(0, klen) === k && (path.length === klen || path.charCodeAt(klen) === 47)) { + base = k; blen = klen; + } + } + return base; +} + function onError(err, req, res, next) { let code = (res.statusCode = err.code || err.status || 500); if (typeof err === 'string' || Buffer.isBuffer(err)) res.end(err); @@ -72,16 +87,20 @@ class Polka extends Router { info = info || this.parse(req); let fns=[], arr=this.wares, obj=this.find(req.method, info.pathname); req.originalUrl = req.originalUrl || req.url; - let base = value(req.path = info.pathname); - if (this.bwares[base] !== void 0) { - arr = arr.concat(this.bwares[base]); + req.path = info.pathname; + let wbase = matchBase(req.path, Object.keys(this.bwares)); + if (wbase !== void 0) { + arr = arr.concat(this.bwares[wbase]); } if (obj) { fns = obj.handlers; req.params = obj.params; - } else if (this.apps[base] !== void 0) { - mutate(base, req); info.pathname=req.path; //=> updates - fns.push(this.apps[base].handler.bind(null, req, res, info)); + } else { + let abase = matchBase(req.path, Object.keys(this.apps)); + if (abase !== void 0) { + mutate(abase, req); info.pathname=req.path; //=> updates + fns.push(this.apps[abase].handler.bind(null, req, res, info)); + } } fns.push(this.onNoMatch); // Grab addl values from `info` diff --git a/tests/polka.js b/tests/polka.js index 29bb29b..a57ab54 100644 --- a/tests/polka.js +++ b/tests/polka.js @@ -309,6 +309,63 @@ test('polka::usage::middleware (basenames)', async t => { app.server.close(); }); +test('polka::usage::middleware (multi-segment basenames)', async t => { + t.plan(11); + + let app = polka() + .use('/foo/bar', (req, res) => { + t.pass('runs the base middleware for: /foo/bar'); + t.false(req.url.includes('/foo/bar'), '~> strips "/foo/bar" base from `req.url`'); + t.false(req.path.includes('/foo/bar'), '~> strips "/foo/bar" base from `req.path`'); + t.ok(req.originalUrl.includes('/foo/bar'), '~> keeps "/foo/bar" base within `req.originalUrl`'); + res.end('hello from foo/bar'); + }) + .get('/', (req, res) => { + t.pass('runs the MAIN app handler for GET /'); + res.end('hello from main'); + }); + + let uri = listen(app); + + // A single-segment base under the same first "folder" should be unaffected. + await axios.get(uri).catch(() => {}); + let r1 = await axios.get(uri); + t.is(r1.status, 200, '~> received 200 status'); + t.is(r1.data, 'hello from main', '~> received "hello from main" response'); + + // Test (GET /foo/bar/baz), a base with more than one "folder" + let r2 = await axios.get(`${uri}/foo/bar/baz`); + t.is(r2.status, 200, '~> received 200 status'); + t.is(r2.data, 'hello from foo/bar', '~> received "hello from foo/bar" response'); + + // A path that only shares the FIRST segment ("/foo") must not match. + await axios.get(`${uri}/foo/baz`).catch(err => { + let r = err.response; + t.is(r.status, 404, '~> "/foo/baz" does not match the "/foo/bar" base'); + }); + + app.server.close(); +}); + +test('polka::usage::sub-application (multi-segment base)', async t => { + t.plan(4); + + let sub = polka().get('/', (req, res) => { + t.pass('runs the sub-application route'); + t.is(req.url, '/', '~> trims the multi-segment base from `req.url`'); + res.end('hello from sub'); + }); + + let app = polka().use('/api/v2', sub); + + let uri = listen(app); + let r = await axios.get(`${uri}/api/v2`); + t.is(r.status, 200, '~> received 200 status'); + t.is(r.data, 'hello from sub', '~> received "hello from sub" response'); + + app.server.close(); +}); + test('polka::usage::middleware (wildcard)', async t => { t.plan(29);