Skip to content

Commit 2020ed0

Browse files
authored
Merge d5d5240 into b0d28c4
2 parents b0d28c4 + d5d5240 commit 2020ed0

2 files changed

Lines changed: 110 additions & 4 deletions

File tree

‎src/middleware/mount/index.test.ts‎

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,84 @@ describe('mount()', () => {
138138
})
139139
})
140140

141+
describe('With dynamic prefixes', () => {
142+
const anotherApp = (req: Request) => new Response(getPath(req))
143+
144+
it.each([
145+
['/api/:version/*', '/api/v1/hello', '/hello'],
146+
['/api/:version/*', '/api/long-version/hello', '/hello'],
147+
['/api/:version/*', '/api/v1', '/'],
148+
['/api/:version/*', '/api/v1/', '/'],
149+
['/api/:version/*', '/api/v1//hello', '//hello'],
150+
['/user/:id{[^/]+}/*', '/user/123/hello', '/hello'],
151+
['/api/:id{[0-9]{2}}/*', '/api/12/hello', '/hello'],
152+
['/x/:prefix{.+}/x/*', '/x/a/b/x/c/d', '/c/d'],
153+
['/x/:prefix{.+}/x/*', '/x/a%2Fb/c/x/d%2Fe', '/d%2Fe'],
154+
['/api/:id{[^/]+}/*', '/api/ad%2Fmin/x%252Fy', '/x%252Fy'],
155+
['/api/:id{[^/]+}/*', '/api/foo%252Fbar/hello', '/hello'],
156+
['/api/:id{[^/]+}/*', '/api/%C3%A9/admin%2Fsecret', '/admin%2Fsecret'],
157+
])('Should strip %s from %s', async (route, path, expected) => {
158+
const app = new Hono()
159+
app.all(route, mount(anotherApp))
160+
161+
const res = await app.request(path)
162+
163+
expect(res.status).toBe(200)
164+
expect(await res.text()).toBe(expected)
165+
})
166+
167+
it('Should use the current route captures with a merged basePath', async () => {
168+
const app = new Hono()
169+
app.use('*', async (_c, next) => {
170+
await next()
171+
})
172+
const subApp = new Hono().basePath('/:tenant')
173+
subApp.all('/app/:id{[^/]+}/*', mount(anotherApp))
174+
app.route('/api', subApp)
175+
176+
const res = await app.request('/api/acme/app/ad%2Fmin/hello')
177+
178+
expect(res.status).toBe(200)
179+
expect(await res.text()).toBe('/hello')
180+
})
181+
182+
it.each([
183+
['/a/*/b/*', '/a/one/b/two/three', '/two/three'],
184+
['/a/*/b/*', '/a/one/b', '/'],
185+
['/foo/*/*', '/foo/x/y/z', '/y/z'],
186+
['/x*/y/*', '/xabc/y/hello', '/hello'],
187+
['/x*/y/*', '/x/y/hello', '/hello'],
188+
['/x*/*', '/x/y', '/y'],
189+
['/x*/*', '/x/hello%2Fworld', '/hello%2Fworld'],
190+
['/x*/y/*', '/x%25/y/foo%252Fbar', '/foo%252Fbar'],
191+
['/assets*', '/assets/hello', '/hello'],
192+
['/assets*', '/assets-v2/hello', '/-v2/hello'],
193+
['/x*/y*', '/xyz/yellow/hello', '/ellow/hello'],
194+
])('Should strip %s up to the final wildcard from %s', async (route, path, expected) => {
195+
const app = new Hono()
196+
app.all(route, mount(anotherApp))
197+
198+
const res = await app.request(path)
199+
200+
expect(res.status).toBe(200)
201+
expect(await res.text()).toBe(expected)
202+
})
203+
204+
it.each([
205+
['/x*/:id/*', '/xabc/123/hello', '/hello'],
206+
['/x*/:id/*', '/x/123/hello', '/hello'],
207+
['/a/*/b/:prefix{.+}/c/*', '/a/one/b/two/three/c/four/five', '/four/five'],
208+
])('Should resolve wildcards and parameters in %s for %s', async (route, path, expected) => {
209+
const app = new Hono()
210+
app.all(route, mount(anotherApp))
211+
212+
const res = await app.request(path)
213+
214+
expect(res.status).toBe(200)
215+
expect(await res.text()).toBe(expected)
216+
})
217+
})
218+
141219
describe('With encoded paths', () => {
142220
const anotherApp = (req: Request) => new Response(getPath(req))
143221

@@ -226,7 +304,7 @@ describe('mount()', () => {
226304
})
227305
})
228306

229-
describe('Mount on `/`', () => {
307+
describe.each(['*', '/*'])('Mount on `%s`', (route) => {
230308
const anotherApp = (req: Request, _params: unknown) => {
231309
const path = getPath(req)
232310
if (path === '/') {
@@ -244,7 +322,7 @@ describe('mount()', () => {
244322
}
245323

246324
const app = new Hono()
247-
app.all('/*', mount(anotherApp))
325+
app.all(route, mount(anotherApp))
248326

249327
it('Should return responses from AnotherApp - mount on `/`', async () => {
250328
let res = await app.request('/')

‎src/middleware/mount/index.ts‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
import type { Context, ExecutionContext } from '../../context'
77
import { routePath } from '../../helper/route'
8+
import { GET_MATCH_RESULT } from '../../request/constants'
89
import type { MiddlewareHandler } from '../../types'
10+
import { getPattern, splitRoutingPath } from '../../utils/url'
911

1012
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1113
type ApplicationHandler = (request: Request, ...args: any) => Response | Promise<Response>
@@ -18,11 +20,37 @@ type MountOptions =
1820
replaceRequest?: MountReplaceRequest | false
1921
}
2022

23+
const getPathPrefixLength = (c: Context, pathPrefix: string): number => {
24+
if (!/[:*]/.test(pathPrefix)) {
25+
return pathPrefix.length
26+
}
27+
28+
const path = c.req.path
29+
const matchResult = c.req[GET_MATCH_RESULT]
30+
let length = 0
31+
for (const part of splitRoutingPath(pathPrefix)) {
32+
const pattern = getPattern(part)
33+
if (pattern && pattern !== '*' && pattern[2] !== true) {
34+
const value =
35+
matchResult.length === 2
36+
? matchResult[1][matchResult[0][c.req.routeIndex][1][pattern[1]]]
37+
: matchResult[0][c.req.routeIndex][1][pattern[1]]
38+
length += value.length + 1
39+
} else if (pattern || part.endsWith('*')) {
40+
const end = path.indexOf('/', length + 1)
41+
length = end === -1 ? path.length : end
42+
} else {
43+
length += part.length + 1
44+
}
45+
}
46+
return length
47+
}
48+
2149
const defaultReplaceRequest = (c: Context): Request => {
2250
// e.g. `/another-app/*` (basePath is already merged) -> `/another-app`
23-
const pathPrefix = routePath(c).replace(/\/\*$/, '')
51+
const pathPrefix = routePath(c).replace(/\/?\*$/, '')
2452
const url = new URL(c.req.raw.url)
25-
url.pathname = c.req.path.slice(pathPrefix.length) || '/'
53+
url.pathname = c.req.path.slice(getPathPrefixLength(c, pathPrefix)) || '/'
2654
return new Request(url, c.req.raw)
2755
}
2856

0 commit comments

Comments
 (0)