Skip to content

Commit 88187ce

Browse files
authored
fix: isolate overlapping static mount paths (#102)
1 parent df403b2 commit 88187ce

2 files changed

Lines changed: 59 additions & 16 deletions

File tree

packages/devframe/src/utils/serve-static.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { tmpdir } from 'node:os'
66
import { join } from 'node:path'
77
import { H3, toNodeHandler } from 'h3'
88
import { afterEach, describe, expect, it } from 'vitest'
9-
import { serveStaticHandler, serveStaticNodeMiddleware } from './serve-static'
9+
import { mountStaticHandler, serveStaticHandler, serveStaticNodeMiddleware } from './serve-static'
1010

1111
interface Fixture {
1212
dir: string
@@ -167,6 +167,55 @@ describe('serveStaticHandler', () => {
167167
})
168168
})
169169

170+
describe('mountStaticHandler', () => {
171+
it('serves files when mounted at the root', async () => {
172+
const dir = makeTmp('devframe-serve-root-')
173+
writeFileSync(join(dir, 'index.html'), 'root-index', 'utf-8')
174+
writeFileSync(join(dir, 'app.js'), 'root-app', 'utf-8')
175+
176+
const app = new H3()
177+
mountStaticHandler(app, '/', dir)
178+
179+
const indexResponse = await app.request('/')
180+
const assetResponse = await app.request('/app.js')
181+
const missingResponse = await app.request('/missing.js')
182+
183+
expect(indexResponse.status).toBe(200)
184+
expect(await indexResponse.text()).toBe('root-index')
185+
expect(assetResponse.status).toBe(200)
186+
expect(await assetResponse.text()).toBe('root-app')
187+
expect(missingResponse.status).toBe(404)
188+
})
189+
190+
it('keeps static bases with overlapping prefixes isolated', async () => {
191+
const viteDir = makeTmp('devframe-serve-vite-')
192+
const vitestDir = makeTmp('devframe-serve-vitest-')
193+
writeFileSync(join(viteDir, 'index.html'), 'vite-index', 'utf-8')
194+
writeFileSync(join(viteDir, 'favicon.svg'), 'vite', 'utf-8')
195+
writeFileSync(join(vitestDir, 'favicon.svg'), 'vitest', 'utf-8')
196+
197+
const app = new H3()
198+
mountStaticHandler(app, '/__devtools-vite/', viteDir)
199+
mountStaticHandler(app, '/__devtools-vitest/', vitestDir)
200+
201+
const viteBaseResponse = await app.request('/__devtools-vite')
202+
const viteBaseSlashResponse = await app.request('/__devtools-vite/')
203+
const viteResponse = await app.request('/__devtools-vite/favicon.svg')
204+
const vitestResponse = await app.request('/__devtools-vitest/favicon.svg')
205+
const adjacentResponse = await app.request('/__devtools-vite-extra/favicon.svg')
206+
207+
expect(viteBaseResponse.status).toBe(200)
208+
expect(await viteBaseResponse.text()).toBe('vite-index')
209+
expect(viteBaseSlashResponse.status).toBe(200)
210+
expect(await viteBaseSlashResponse.text()).toBe('vite-index')
211+
expect(viteResponse.status).toBe(200)
212+
expect(await viteResponse.text()).toBe('vite')
213+
expect(vitestResponse.status).toBe(200)
214+
expect(await vitestResponse.text()).toBe('vitest')
215+
expect(adjacentResponse.status).toBe(404)
216+
})
217+
})
218+
170219
describe('serveStaticNodeMiddleware', () => {
171220
let fx: Fixture | undefined
172221

packages/devframe/src/utils/serve-static.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { EventHandler, H3 } from 'h3'
1+
import type { EventHandler } from 'h3'
22
import type { IncomingMessage, ServerResponse } from 'node:http'
33
import { createReadStream } from 'node:fs'
44
import { stat } from 'node:fs/promises'
55
import { Readable } from 'node:stream'
6-
import { defineHandler, withBase } from 'h3'
6+
import { defineHandler, H3 } from 'h3'
77
import { lookup } from 'mrmime'
88
import { extname, join, normalize, resolve, sep } from 'pathe'
99

@@ -168,27 +168,21 @@ export function serveStaticHandler(
168168
}
169169

170170
/**
171-
* Mount {@link serveStaticHandler} on an h3 app at `base`, handling the
172-
* route pattern and prefix-stripping required by h3 v2.
171+
* Mount {@link serveStaticHandler} on an h3 app at `base`.
173172
*
174-
* h3 v2's `app.use(base, handler)` only matches the exact `base` path and
175-
* does not strip the prefix from `event.url.pathname`. Static serving
176-
* needs both subpath matching (`/base/**`) and the URL stripped so the
177-
* file resolver sees paths relative to `dir` — this helper bundles both.
173+
* h3's sub-app mount provides segment-boundary matching and strips `base`
174+
* from `event.url.pathname`, so the file resolver sees paths relative to
175+
* `dir`.
178176
*/
179177
export function mountStaticHandler(
180178
app: H3,
181179
base: string,
182180
dir: string,
183181
options?: ServeStaticOptions,
184182
): void {
185-
const trimmed = base.replace(/\/$/, '')
186-
const handler = serveStaticHandler(dir, options)
187-
if (trimmed === '') {
188-
app.use('/**', handler)
189-
return
190-
}
191-
app.use(`${trimmed}/**`, withBase(trimmed, handler))
183+
const staticApp = new H3()
184+
staticApp.use(serveStaticHandler(dir, options))
185+
app.mount(base.replace(/\/$/, ''), staticApp)
192186
}
193187

194188
/**

0 commit comments

Comments
 (0)