@fastify/static 9.1.2 → 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 +104 -10
- package/package.json +4 -2
- package/test/static.test.js +153 -22
|
@@ -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,23 +573,113 @@ function getEncodingHeader (headers, checked) {
|
|
|
569
573
|
}
|
|
570
574
|
|
|
571
575
|
/**
|
|
572
|
-
* @param {string}
|
|
576
|
+
* @param {string} routePrefix
|
|
577
|
+
* @returns {Array<string|undefined>}
|
|
578
|
+
*/
|
|
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
|
+
}
|
|
602
|
+
|
|
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
|
+
}
|
|
631
|
+
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const tokenEnd = pathnameIndex + token.length
|
|
636
|
+
if (tokenEnd > pathnameEnd || !pathname.startsWith(token, pathnameIndex)) {
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
pathnameIndex = tokenEnd
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return pathnameIndex
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
573
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
|
|
656
|
+
}
|
|
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
|
|
574
671
|
* @returns {string|undefined}
|
|
575
672
|
*/
|
|
576
|
-
function getPathnameForSend (url,
|
|
673
|
+
function getPathnameForSend (url, matchRoutePrefix) {
|
|
577
674
|
const questionMark = url.indexOf('?')
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const routePrefix = route.endsWith('*')
|
|
581
|
-
? route.slice(0, -1)
|
|
582
|
-
: route
|
|
675
|
+
const pathnameEnd = questionMark === -1 ? url.length : questionMark
|
|
583
676
|
|
|
584
|
-
|
|
677
|
+
const prefixLength = matchRoutePrefix(url, pathnameEnd)
|
|
678
|
+
if (prefixLength === undefined) {
|
|
585
679
|
return
|
|
586
680
|
}
|
|
587
681
|
|
|
588
|
-
pathname =
|
|
682
|
+
let pathname = url.slice(prefixLength, pathnameEnd)
|
|
589
683
|
|
|
590
684
|
if (pathname === '') {
|
|
591
685
|
pathname = '/'
|
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
|
@@ -6,7 +6,6 @@ const path = require('node:path')
|
|
|
6
6
|
const fs = require('node:fs')
|
|
7
7
|
const url = require('node:url')
|
|
8
8
|
const http = require('node:http')
|
|
9
|
-
const Module = require('node:module')
|
|
10
9
|
const { test } = require('node:test')
|
|
11
10
|
const Fastify = require('fastify')
|
|
12
11
|
const compress = require('@fastify/compress')
|
|
@@ -82,16 +81,6 @@ if (typeof Promise.withResolvers === 'undefined') {
|
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
function loadInternalPathnameForSend () {
|
|
86
|
-
const modulePath = require.resolve('../index.js')
|
|
87
|
-
const source = fs.readFileSync(modulePath, 'utf8') + '\nmodule.exports.__getPathnameForSend = getPathnameForSend\n'
|
|
88
|
-
const testModule = new Module(modulePath)
|
|
89
|
-
testModule.filename = modulePath
|
|
90
|
-
testModule.paths = module.paths
|
|
91
|
-
testModule._compile(source, modulePath)
|
|
92
|
-
return testModule.exports.__getPathnameForSend
|
|
93
|
-
}
|
|
94
|
-
|
|
95
84
|
test('register /static prefixAvoidTrailingSlash', async t => {
|
|
96
85
|
t.plan(11)
|
|
97
86
|
|
|
@@ -3601,16 +3590,6 @@ test(
|
|
|
3601
3590
|
}
|
|
3602
3591
|
)
|
|
3603
3592
|
|
|
3604
|
-
test('getPathnameForSend returns undefined for mismatched and malformed routes', (t) => {
|
|
3605
|
-
t.plan(3)
|
|
3606
|
-
|
|
3607
|
-
const getPathnameForSend = loadInternalPathnameForSend()
|
|
3608
|
-
|
|
3609
|
-
t.assert.deepStrictEqual(getPathnameForSend('/nested/public/index.css', '/public/*'), undefined)
|
|
3610
|
-
t.assert.deepStrictEqual(getPathnameForSend('/static/%E0%A4%A', '/static/*'), undefined)
|
|
3611
|
-
t.assert.deepStrictEqual(getPathnameForSend('/static/index.css', '/static'), '/index.css')
|
|
3612
|
-
})
|
|
3613
|
-
|
|
3614
3593
|
test('wildcard handler falls back to not found when the raw url does not match the route prefix', async (t) => {
|
|
3615
3594
|
t.plan(2)
|
|
3616
3595
|
|
|
@@ -3634,7 +3613,7 @@ test('wildcard handler falls back to not found when the raw url does not match t
|
|
|
3634
3613
|
t.assert.ok(wildcardHandler)
|
|
3635
3614
|
|
|
3636
3615
|
let notFoundCalled = false
|
|
3637
|
-
wildcardHandler({
|
|
3616
|
+
await wildcardHandler({
|
|
3638
3617
|
raw: { url: '/nested/static/index.css' },
|
|
3639
3618
|
routeOptions: { url: '/static/*' }
|
|
3640
3619
|
}, {
|
|
@@ -3646,6 +3625,111 @@ test('wildcard handler falls back to not found when the raw url does not match t
|
|
|
3646
3625
|
t.assert.deepStrictEqual(notFoundCalled, true)
|
|
3647
3626
|
})
|
|
3648
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
|
+
|
|
3649
3733
|
test('does not serve static files with encoded path separators', async (t) => {
|
|
3650
3734
|
t.plan(4)
|
|
3651
3735
|
|
|
@@ -3701,6 +3785,53 @@ test('serves wildcard files when registered in an encapsulated context', async (
|
|
|
3701
3785
|
t.assert.deepStrictEqual(response.body, fs.readFileSync(path.join(__dirname, '/static/index.css'), 'utf8'))
|
|
3702
3786
|
})
|
|
3703
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
|
+
|
|
3704
3835
|
test('content-length in head route should not return zero when using wildcard', async t => {
|
|
3705
3836
|
t.plan(5)
|
|
3706
3837
|
|