@fastify/static 9.1.1 → 9.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/example/server-benchmark.js +39 -0
- package/index.js +105 -13
- package/package.json +4 -2
- package/test/static.test.js +212 -0
- package/test/static/shallow/no-link +0 -1
- package/test/static-symbolic-link/dir/symlink +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const fastify = require('fastify')({ logger: false })
|
|
5
|
+
const fastifyStatic = require(process.env.PLUGIN_PATH || '../')
|
|
6
|
+
|
|
7
|
+
const root = path.join(__dirname, '/public')
|
|
8
|
+
const port = Number(process.env.PORT || 3000)
|
|
9
|
+
|
|
10
|
+
fastify.register(fastifyStatic, {
|
|
11
|
+
root,
|
|
12
|
+
prefix: '/static',
|
|
13
|
+
decorateReply: false
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
fastify.register(fastifyStatic, {
|
|
17
|
+
root,
|
|
18
|
+
prefix: '/app/:version',
|
|
19
|
+
decorateReply: false
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
fastify.register(async function (child) {
|
|
23
|
+
child.register(fastifyStatic, {
|
|
24
|
+
root,
|
|
25
|
+
prefix: '/public',
|
|
26
|
+
decorateReply: false
|
|
27
|
+
})
|
|
28
|
+
}, { prefix: '/nested' })
|
|
29
|
+
|
|
30
|
+
fastify.listen({ port }, err => {
|
|
31
|
+
if (err) throw err
|
|
32
|
+
|
|
33
|
+
console.log(`benchmark server listening on http://127.0.0.1:${port}`)
|
|
34
|
+
console.log('')
|
|
35
|
+
console.log('Try:')
|
|
36
|
+
console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/static/index.css`)
|
|
37
|
+
console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/app/1.2.3/index.css`)
|
|
38
|
+
console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/nested/public/index.css`)
|
|
39
|
+
})
|
package/index.js
CHANGED
|
@@ -117,12 +117,16 @@ async function fastifyStatic (fastify, opts) {
|
|
|
117
117
|
throw new TypeError('"wildcard" option must be a boolean')
|
|
118
118
|
}
|
|
119
119
|
if (opts.wildcard === undefined || opts.wildcard === true) {
|
|
120
|
+
let matchRoutePrefix
|
|
121
|
+
|
|
120
122
|
fastify.route({
|
|
121
123
|
...routeOpts,
|
|
122
124
|
method: ['HEAD', 'GET'],
|
|
123
125
|
path: prefix + '*',
|
|
124
126
|
handler (req, reply) {
|
|
125
|
-
|
|
127
|
+
matchRoutePrefix ??= createRoutePrefixMatcher(req.routeOptions.url)
|
|
128
|
+
|
|
129
|
+
const pathname = getPathnameForSend(req.raw.url, matchRoutePrefix)
|
|
126
130
|
if (!pathname) {
|
|
127
131
|
return reply.callNotFound()
|
|
128
132
|
}
|
|
@@ -569,26 +573,114 @@ function getEncodingHeader (headers, checked) {
|
|
|
569
573
|
}
|
|
570
574
|
|
|
571
575
|
/**
|
|
572
|
-
* @param {string}
|
|
573
|
-
* @
|
|
574
|
-
* @returns {string|undefined}
|
|
576
|
+
* @param {string} routePrefix
|
|
577
|
+
* @returns {Array<string|undefined>}
|
|
575
578
|
*/
|
|
576
|
-
function
|
|
577
|
-
const
|
|
578
|
-
let
|
|
579
|
+
function createRoutePrefixTokens (routePrefix) {
|
|
580
|
+
const tokens = []
|
|
581
|
+
let routeIndex = 0
|
|
582
|
+
let segmentStart = 0
|
|
583
|
+
|
|
584
|
+
while (routeIndex < routePrefix.length) {
|
|
585
|
+
if (routePrefix[routeIndex] !== ':') {
|
|
586
|
+
routeIndex++
|
|
587
|
+
continue
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (segmentStart !== routeIndex) {
|
|
591
|
+
tokens.push(routePrefix.slice(segmentStart, routeIndex))
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
routeIndex++
|
|
595
|
+
while (routeIndex < routePrefix.length && routePrefix[routeIndex] !== '/') {
|
|
596
|
+
routeIndex++
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
tokens.push(undefined)
|
|
600
|
+
segmentStart = routeIndex
|
|
601
|
+
}
|
|
579
602
|
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
603
|
+
if (segmentStart !== routePrefix.length) {
|
|
604
|
+
tokens.push(routePrefix.slice(segmentStart))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return tokens
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* @param {string} pathname
|
|
612
|
+
* @param {number} pathnameEnd
|
|
613
|
+
* @param {Array<string|undefined>} tokens
|
|
614
|
+
* @returns {number|undefined}
|
|
615
|
+
*/
|
|
616
|
+
function getRoutePrefixMatchLength (pathname, pathnameEnd, tokens) {
|
|
617
|
+
let pathnameIndex = 0
|
|
618
|
+
|
|
619
|
+
for (const token of tokens) {
|
|
620
|
+
if (token === undefined) {
|
|
621
|
+
const segmentStart = pathnameIndex
|
|
622
|
+
const slashIndex = pathname.indexOf('/', pathnameIndex)
|
|
623
|
+
|
|
624
|
+
pathnameIndex = slashIndex === -1 || slashIndex > pathnameEnd
|
|
625
|
+
? pathnameEnd
|
|
626
|
+
: slashIndex
|
|
627
|
+
|
|
628
|
+
if (pathnameIndex === segmentStart) {
|
|
629
|
+
return
|
|
630
|
+
}
|
|
584
631
|
|
|
585
|
-
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const tokenEnd = pathnameIndex + token.length
|
|
636
|
+
if (tokenEnd > pathnameEnd || !pathname.startsWith(token, pathnameIndex)) {
|
|
586
637
|
return
|
|
587
638
|
}
|
|
588
639
|
|
|
589
|
-
|
|
640
|
+
pathnameIndex = tokenEnd
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return pathnameIndex
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* @param {string} route
|
|
648
|
+
* @returns {(pathname: string, pathnameEnd: number) => number|undefined}
|
|
649
|
+
*/
|
|
650
|
+
function createRoutePrefixMatcher (route) {
|
|
651
|
+
const routePrefix = route.replace(/\*$/u, '')
|
|
652
|
+
const routePrefixLength = routePrefix.length
|
|
653
|
+
|
|
654
|
+
if (routePrefix === '/') {
|
|
655
|
+
return () => 0
|
|
590
656
|
}
|
|
591
657
|
|
|
658
|
+
if (routePrefix.includes(':') === false) {
|
|
659
|
+
return (pathname, pathnameEnd) => routePrefixLength <= pathnameEnd && pathname.startsWith(routePrefix)
|
|
660
|
+
? routePrefixLength
|
|
661
|
+
: undefined
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const tokens = createRoutePrefixTokens(routePrefix)
|
|
665
|
+
return (pathname, pathnameEnd) => getRoutePrefixMatchLength(pathname, pathnameEnd, tokens)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* @param {string} url
|
|
670
|
+
* @param {(pathname: string, pathnameEnd: number) => number|undefined} matchRoutePrefix
|
|
671
|
+
* @returns {string|undefined}
|
|
672
|
+
*/
|
|
673
|
+
function getPathnameForSend (url, matchRoutePrefix) {
|
|
674
|
+
const questionMark = url.indexOf('?')
|
|
675
|
+
const pathnameEnd = questionMark === -1 ? url.length : questionMark
|
|
676
|
+
|
|
677
|
+
const prefixLength = matchRoutePrefix(url, pathnameEnd)
|
|
678
|
+
if (prefixLength === undefined) {
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let pathname = url.slice(prefixLength, pathnameEnd)
|
|
683
|
+
|
|
592
684
|
if (pathname === '') {
|
|
593
685
|
pathname = '/'
|
|
594
686
|
} else if (!pathname.startsWith('/')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/static",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.3",
|
|
4
4
|
"description": "Plugin for serving static files as fast as possible.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"test": "npm run test:unit && npm run test:typescript",
|
|
13
13
|
"test:typescript": "tsd",
|
|
14
14
|
"test:unit": "borp -C --check-coverage --lines 100",
|
|
15
|
-
"example": "node example/server.js"
|
|
15
|
+
"example": "node example/server.js",
|
|
16
|
+
"example:benchmark": "node example/server-benchmark.js"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"devDependencies": {
|
|
69
70
|
"@fastify/compress": "^8.0.0",
|
|
70
71
|
"@types/node": "^25.0.3",
|
|
72
|
+
"autocannon": "^8.0.0",
|
|
71
73
|
"borp": "^1.0.0",
|
|
72
74
|
"c8": "^11.0.0",
|
|
73
75
|
"concat-stream": "^2.0.0",
|
package/test/static.test.js
CHANGED
|
@@ -3590,6 +3590,146 @@ test(
|
|
|
3590
3590
|
}
|
|
3591
3591
|
)
|
|
3592
3592
|
|
|
3593
|
+
test('wildcard handler falls back to not found when the raw url does not match the route prefix', async (t) => {
|
|
3594
|
+
t.plan(2)
|
|
3595
|
+
|
|
3596
|
+
const fastify = Fastify()
|
|
3597
|
+
let wildcardHandler
|
|
3598
|
+
|
|
3599
|
+
fastify.addHook('onRoute', (route) => {
|
|
3600
|
+
if (route.url === '/static/*') {
|
|
3601
|
+
wildcardHandler = route.handler
|
|
3602
|
+
}
|
|
3603
|
+
})
|
|
3604
|
+
|
|
3605
|
+
fastify.register(fastifyStatic, {
|
|
3606
|
+
root: path.join(__dirname, '/static'),
|
|
3607
|
+
prefix: '/static'
|
|
3608
|
+
})
|
|
3609
|
+
|
|
3610
|
+
t.after(() => fastify.close())
|
|
3611
|
+
|
|
3612
|
+
await fastify.ready()
|
|
3613
|
+
t.assert.ok(wildcardHandler)
|
|
3614
|
+
|
|
3615
|
+
let notFoundCalled = false
|
|
3616
|
+
await wildcardHandler({
|
|
3617
|
+
raw: { url: '/nested/static/index.css' },
|
|
3618
|
+
routeOptions: { url: '/static/*' }
|
|
3619
|
+
}, {
|
|
3620
|
+
callNotFound () {
|
|
3621
|
+
notFoundCalled = true
|
|
3622
|
+
}
|
|
3623
|
+
})
|
|
3624
|
+
|
|
3625
|
+
t.assert.deepStrictEqual(notFoundCalled, true)
|
|
3626
|
+
})
|
|
3627
|
+
|
|
3628
|
+
test('wildcard handler falls back to not found when a param segment is empty', async (t) => {
|
|
3629
|
+
t.plan(2)
|
|
3630
|
+
|
|
3631
|
+
const fastify = Fastify()
|
|
3632
|
+
let wildcardHandler
|
|
3633
|
+
|
|
3634
|
+
fastify.addHook('onRoute', (route) => {
|
|
3635
|
+
if (route.url === '/app/:version/*') {
|
|
3636
|
+
wildcardHandler = route.handler
|
|
3637
|
+
}
|
|
3638
|
+
})
|
|
3639
|
+
|
|
3640
|
+
fastify.register(fastifyStatic, {
|
|
3641
|
+
root: path.join(__dirname, '/static'),
|
|
3642
|
+
prefix: '/app/:version'
|
|
3643
|
+
})
|
|
3644
|
+
|
|
3645
|
+
t.after(() => fastify.close())
|
|
3646
|
+
|
|
3647
|
+
await fastify.ready()
|
|
3648
|
+
t.assert.ok(wildcardHandler)
|
|
3649
|
+
|
|
3650
|
+
let notFoundCalled = false
|
|
3651
|
+
await wildcardHandler({
|
|
3652
|
+
raw: { url: '/app//index.css' },
|
|
3653
|
+
routeOptions: { url: '/app/:version/*' }
|
|
3654
|
+
}, {
|
|
3655
|
+
callNotFound () {
|
|
3656
|
+
notFoundCalled = true
|
|
3657
|
+
}
|
|
3658
|
+
})
|
|
3659
|
+
|
|
3660
|
+
t.assert.deepStrictEqual(notFoundCalled, true)
|
|
3661
|
+
})
|
|
3662
|
+
|
|
3663
|
+
test('wildcard handler falls back to not found when the wildcard remainder is malformed', async (t) => {
|
|
3664
|
+
t.plan(2)
|
|
3665
|
+
|
|
3666
|
+
const fastify = Fastify()
|
|
3667
|
+
let wildcardHandler
|
|
3668
|
+
|
|
3669
|
+
fastify.addHook('onRoute', (route) => {
|
|
3670
|
+
if (route.url === '/app/:version/*') {
|
|
3671
|
+
wildcardHandler = route.handler
|
|
3672
|
+
}
|
|
3673
|
+
})
|
|
3674
|
+
|
|
3675
|
+
fastify.register(fastifyStatic, {
|
|
3676
|
+
root: path.join(__dirname, '/static'),
|
|
3677
|
+
prefix: '/app/:version'
|
|
3678
|
+
})
|
|
3679
|
+
|
|
3680
|
+
t.after(() => fastify.close())
|
|
3681
|
+
|
|
3682
|
+
await fastify.ready()
|
|
3683
|
+
t.assert.ok(wildcardHandler)
|
|
3684
|
+
|
|
3685
|
+
let notFoundCalled = false
|
|
3686
|
+
await wildcardHandler({
|
|
3687
|
+
raw: { url: '/app/1.2.3/%E0%A4%A' },
|
|
3688
|
+
routeOptions: { url: '/app/:version/*' }
|
|
3689
|
+
}, {
|
|
3690
|
+
callNotFound () {
|
|
3691
|
+
notFoundCalled = true
|
|
3692
|
+
}
|
|
3693
|
+
})
|
|
3694
|
+
|
|
3695
|
+
t.assert.deepStrictEqual(notFoundCalled, true)
|
|
3696
|
+
})
|
|
3697
|
+
|
|
3698
|
+
test('wildcard handler falls back to not found when a literal segment after a param does not match', async (t) => {
|
|
3699
|
+
t.plan(2)
|
|
3700
|
+
|
|
3701
|
+
const fastify = Fastify()
|
|
3702
|
+
let wildcardHandler
|
|
3703
|
+
|
|
3704
|
+
fastify.addHook('onRoute', (route) => {
|
|
3705
|
+
if (route.url === '/app/:version/public/*') {
|
|
3706
|
+
wildcardHandler = route.handler
|
|
3707
|
+
}
|
|
3708
|
+
})
|
|
3709
|
+
|
|
3710
|
+
fastify.register(fastifyStatic, {
|
|
3711
|
+
root: path.join(__dirname, '/static'),
|
|
3712
|
+
prefix: '/app/:version/public'
|
|
3713
|
+
})
|
|
3714
|
+
|
|
3715
|
+
t.after(() => fastify.close())
|
|
3716
|
+
|
|
3717
|
+
await fastify.ready()
|
|
3718
|
+
t.assert.ok(wildcardHandler)
|
|
3719
|
+
|
|
3720
|
+
let notFoundCalled = false
|
|
3721
|
+
await wildcardHandler({
|
|
3722
|
+
raw: { url: '/app/1.2.3/private/index.css' },
|
|
3723
|
+
routeOptions: { url: '/app/:version/public/*' }
|
|
3724
|
+
}, {
|
|
3725
|
+
callNotFound () {
|
|
3726
|
+
notFoundCalled = true
|
|
3727
|
+
}
|
|
3728
|
+
})
|
|
3729
|
+
|
|
3730
|
+
t.assert.deepStrictEqual(notFoundCalled, true)
|
|
3731
|
+
})
|
|
3732
|
+
|
|
3593
3733
|
test('does not serve static files with encoded path separators', async (t) => {
|
|
3594
3734
|
t.plan(4)
|
|
3595
3735
|
|
|
@@ -3620,6 +3760,78 @@ test('does not serve static files with encoded path separators', async (t) => {
|
|
|
3620
3760
|
t.assert.deepStrictEqual(encodedPathResponse.json().message, 'Route GET:/deep%2Fpath/for/test/purpose/foo.html not found')
|
|
3621
3761
|
})
|
|
3622
3762
|
|
|
3763
|
+
test('serves wildcard files when registered in an encapsulated context', async (t) => {
|
|
3764
|
+
t.plan(3)
|
|
3765
|
+
|
|
3766
|
+
const fastify = Fastify()
|
|
3767
|
+
|
|
3768
|
+
t.after(() => fastify.close())
|
|
3769
|
+
|
|
3770
|
+
fastify.register(async function (childContext) {
|
|
3771
|
+
childContext.register(fastifyStatic, {
|
|
3772
|
+
root: path.join(__dirname, '/static'),
|
|
3773
|
+
prefix: '/public',
|
|
3774
|
+
decorateReply: false
|
|
3775
|
+
})
|
|
3776
|
+
}, { prefix: '/nested' })
|
|
3777
|
+
|
|
3778
|
+
const response = await fastify.inject({
|
|
3779
|
+
method: 'GET',
|
|
3780
|
+
url: '/nested/public/index.css'
|
|
3781
|
+
})
|
|
3782
|
+
|
|
3783
|
+
t.assert.deepStrictEqual(response.statusCode, 200)
|
|
3784
|
+
t.assert.deepStrictEqual(response.headers['content-type'], 'text/css; charset=utf-8')
|
|
3785
|
+
t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.css'), 'utf8'))
|
|
3786
|
+
})
|
|
3787
|
+
|
|
3788
|
+
test('serves wildcard files when prefix contains a route param', async (t) => {
|
|
3789
|
+
t.plan(3)
|
|
3790
|
+
|
|
3791
|
+
const fastify = Fastify()
|
|
3792
|
+
|
|
3793
|
+
t.after(() => fastify.close())
|
|
3794
|
+
|
|
3795
|
+
fastify.register(fastifyStatic, {
|
|
3796
|
+
root: path.join(__dirname, '/static'),
|
|
3797
|
+
prefix: '/app/:version',
|
|
3798
|
+
decorateReply: false
|
|
3799
|
+
})
|
|
3800
|
+
|
|
3801
|
+
const response = await fastify.inject({
|
|
3802
|
+
method: 'GET',
|
|
3803
|
+
url: '/app/1.2.3/index.css'
|
|
3804
|
+
})
|
|
3805
|
+
|
|
3806
|
+
t.assert.deepStrictEqual(response.statusCode, 200)
|
|
3807
|
+
t.assert.deepStrictEqual(response.headers['content-type'], 'text/css; charset=utf-8')
|
|
3808
|
+
t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.css'), 'utf8'))
|
|
3809
|
+
})
|
|
3810
|
+
|
|
3811
|
+
test('serves wildcard index files when a param prefix uses prefixAvoidTrailingSlash', async (t) => {
|
|
3812
|
+
t.plan(3)
|
|
3813
|
+
|
|
3814
|
+
const fastify = Fastify()
|
|
3815
|
+
|
|
3816
|
+
t.after(() => fastify.close())
|
|
3817
|
+
|
|
3818
|
+
fastify.register(fastifyStatic, {
|
|
3819
|
+
root: path.join(__dirname, '/static'),
|
|
3820
|
+
prefix: '/app/:version',
|
|
3821
|
+
prefixAvoidTrailingSlash: true,
|
|
3822
|
+
decorateReply: false
|
|
3823
|
+
})
|
|
3824
|
+
|
|
3825
|
+
const response = await fastify.inject({
|
|
3826
|
+
method: 'GET',
|
|
3827
|
+
url: '/app/1.2.3'
|
|
3828
|
+
})
|
|
3829
|
+
|
|
3830
|
+
t.assert.deepStrictEqual(response.statusCode, 200)
|
|
3831
|
+
t.assert.deepStrictEqual(response.headers['content-type'], 'text/html; charset=utf-8')
|
|
3832
|
+
t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.html'), 'utf8'))
|
|
3833
|
+
})
|
|
3834
|
+
|
|
3623
3835
|
test('content-length in head route should not return zero when using wildcard', async t => {
|
|
3624
3836
|
t.plan(5)
|
|
3625
3837
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/none
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
../origin
|