UNPKG

@fastify/static

Version:

Plugin for serving static files as fast as possible.

1,518 lines (1,201 loc) 116 kB
'use strict' /* eslint n/no-deprecated-api: "off" */ const path = require('node:path') const fs = require('node:fs') const url = require('node:url') const http = require('node:http') const { test } = require('node:test') const Fastify = require('fastify') const compress = require('@fastify/compress') const concat = require('concat-stream') const pino = require('pino') const proxyquire = require('proxyquire') const fastifyStatic = require('../') const indexContent = fs .readFileSync('./test/static/index.html') .toString('utf8') const index2Content = fs .readFileSync('./test/static2/index.html') .toString('utf8') const foobarContent = fs .readFileSync('./test/static/foobar.html') .toString('utf8') const deepContent = fs .readFileSync('./test/static/deep/path/for/test/purpose/foo.html') .toString('utf8') const innerIndex = fs .readFileSync('./test/static/deep/path/for/test/index.html') .toString('utf8') const allThreeBr = fs.readFileSync( './test/static-pre-compressed/all-three.html.br' ) const allThreeGzip = fs.readFileSync( './test/static-pre-compressed/all-three.html.gz' ) const gzipOnly = fs.readFileSync( './test/static-pre-compressed/gzip-only.html.gz' ) const indexBr = fs.readFileSync( './test/static-pre-compressed/index.html.br' ) const dirIndexBr = fs.readFileSync( './test/static-pre-compressed/dir/index.html.br' ) const dirIndexGz = fs.readFileSync( './test/static-pre-compressed/dir-gz/index.html.gz' ) const uncompressedStatic = fs .readFileSync('./test/static-pre-compressed/uncompressed.html') .toString('utf8') const fooContent = fs.readFileSync('./test/static/foo.html').toString('utf8') const barContent = fs.readFileSync('./test/static2/bar.html').toString('utf8') const jsonHiddenContent = fs.readFileSync('./test/static-hidden/.hidden/sample.json').toString('utf8') const GENERIC_RESPONSE_CHECK_COUNT = 5 function genericResponseChecks (t, response) { t.assert.ok(/text\/(html|css)/.test(response.headers.get?.('content-type') ?? response.headers['content-type'])) t.assert.ok(response.headers.get?.('etag') ?? response.headers.etag) t.assert.ok(response.headers.get?.('last-modified') ?? response.headers['last-modified']) t.assert.ok(response.headers.get?.('date') ?? response.headers.date) t.assert.ok(response.headers.get?.('cache-control') ?? response.headers['cache-control']) } const GENERIC_ERROR_RESPONSE_CHECK_COUNT = 2 function genericErrorResponseChecks (t, response) { t.assert.deepStrictEqual(response.headers.get?.('content-type') ?? response.headers['content-type'], 'application/json; charset=utf-8') t.assert.ok(response.headers.get?.('date') ?? response.headers.date) } if (typeof Promise.withResolvers === 'undefined') { Promise.withResolvers = function () { let promiseResolve, promiseReject const promise = new Promise((resolve, reject) => { promiseResolve = resolve promiseReject = reject }) return { promise, resolve: promiseResolve, reject: promiseReject } } } test('register /static prefixAvoidTrailingSlash', async t => { t.plan(11) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static', prefixAvoidTrailingSlash: true } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static/index.css', async (t) => { t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.css') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) genericResponseChecks(t, response) }) await t.test('/static/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static/deep/path/for/test/purpose/foo.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/purpose/foo.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) await t.test('/static/deep/path/for/test/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), innerIndex) genericResponseChecks(t, response) }) await t.test('/static/this/path/for/test', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/for/test') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/static/this/path/doesnt/exist.html', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/doesnt/exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/static/../index.js', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/../index.js') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('file not exposed outside of the plugin', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foobar.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) }) await t.test('file retrieve with HEAD method', async t => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html', { method: 'HEAD' }) t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), '') genericResponseChecks(t, response) }) }) test('register /static', async (t) => { t.plan(10) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static' } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static/index.css', async (t) => { t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.css') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) genericResponseChecks(t, response) }) await t.test('/static/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) }) await t.test('/static/deep/path/for/test/purpose/foo.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/purpose/foo.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) await t.test('/static/deep/path/for/test/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), innerIndex) genericResponseChecks(t, response) }) await t.test('/static/this/path/for/test', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/for/test') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/static/this/path/doesnt/exist.html', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/doesnt/exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/static/../index.js', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/../index.js', { redirect: 'error' }) t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('file not exposed outside of the plugin', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foobar.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) }) }) test('register /static/', async t => { t.plan(11) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static/' } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static/index.html', async t => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html', { method: 'HEAD' }) t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), '') genericResponseChecks(t, response) }) await t.test('/static/index.css', async (t) => { t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.css') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) genericResponseChecks(t, response) }) await t.test('/static/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/static', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) }) await t.test('/static/deep/path/for/test/purpose/foo.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/purpose/foo.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) await t.test('/static/deep/path/for/test/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/deep/path/for/test/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), innerIndex) genericResponseChecks(t, response) }) await t.test('/static/this/path/for/test', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/for/test') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/static/this/path/doesnt/exist.html', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/doesnt/exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/static/../index.js', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/../index.js') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('304', async t => { t.plan(5 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) const response2 = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html', { headers: { 'if-none-match': response.headers.get('etag') }, cache: 'no-cache' }) t.assert.ok(!response2.ok) t.assert.deepStrictEqual(response2.status, 304) }) }) test('register /static and /static2', async (t) => { t.plan(4) const pluginOptions = { root: [path.join(__dirname, '/static'), path.join(__dirname, '/static2')], prefix: '/static' } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) fastify.get('/foo', (_req, rep) => { rep.sendFile('foo.html') }) fastify.get('/bar', (_req, rep) => { rep.sendFile('bar.html') }) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) const responseText = await response.text() t.assert.deepStrictEqual(responseText, indexContent) t.assert.notDeepStrictEqual(responseText, index2Content) genericResponseChecks(t, response) }) await t.test('/static/bar.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/bar.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), barContent) genericResponseChecks(t, response) }) await t.test('sendFile foo.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), fooContent) genericResponseChecks(t, response) }) await t.test('sendFile bar.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), barContent) genericResponseChecks(t, response) }) }) test('register /static with constraints', async (t) => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static', constraints: { version: '1.2.0' } } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('example.com/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html', { headers: { 'accept-version': '1.x' } }) t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('not-example.com/static/index.html', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html', { headers: { 'accept-version': '2.x' } }) t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) }) test('payload.path is set', async (t) => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static/' } const fastify = Fastify() let gotFilename fastify.register(fastifyStatic, pluginOptions) fastify.addHook('onSend', function (_req, _reply, payload, next) { gotFilename = payload.path next() }) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(5 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) t.assert.deepStrictEqual(typeof gotFilename, 'string') t.assert.deepStrictEqual(gotFilename, path.join(pluginOptions.root, 'index.html')) genericResponseChecks(t, response) }) await t.test('/static/this/path/doesnt/exist.html', async (t) => { t.plan(3 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/this/path/doesnt/exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) t.assert.deepStrictEqual(typeof gotFilename, 'undefined') genericErrorResponseChecks(t, response) }) }) test('error responses can be customized with fastify.setErrorHandler()', async t => { t.plan(1) const pluginOptions = { root: path.join(__dirname, '/static') } const fastify = Fastify() fastify.setErrorHandler(function errorHandler (err, _request, reply) { reply.code(403).type('text/plain').send(`${err.statusCode} Custom error message`) }) fastify.get('/index.js', (_, reply) => { return reply.type('text/html').sendFile('foo.js') }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/../index.js', async t => { t.plan(4) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.js') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 403) t.assert.deepStrictEqual(response.headers.get('content-type'), 'text/plain') t.assert.deepStrictEqual(await response.text(), '500 Custom error message') }) }) test('not found responses can be customized with fastify.setNotFoundHandler()', async t => { t.plan(1) const pluginOptions = { root: path.join(__dirname, '/static') } const fastify = Fastify() fastify.setNotFoundHandler(function notFoundHandler (request, reply) { reply.code(404).type('text/plain').send(request.raw.url + ' Not Found') }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/path/does/not/exist.html', async t => { t.plan(4) const response = await fetch('http://localhost:' + fastify.server.address().port + '/path/does/not/exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) t.assert.deepStrictEqual(response.headers.get('content-type'), 'text/plain') t.assert.deepStrictEqual(await response.text(), '/path/does/not/exist.html Not Found') }) }) test('fastify.setNotFoundHandler() is called for dotfiles when send is configured to ignore dotfiles', async t => { t.plan(1) const pluginOptions = { root: path.join(__dirname, '/static'), send: { dotfiles: 'ignore' } } const fastify = Fastify() fastify.setNotFoundHandler(function notFoundHandler (request, reply) { reply.code(404).type('text/plain').send(request.raw.url + ' Not Found') }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() // Requesting files with a leading dot doesn't follow the same code path as // other 404 errors await t.test('/path/does/not/.exist.html', async t => { t.plan(4) const response = await fetch('http://localhost:' + fastify.server.address().port + '/path/does/not/.exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) t.assert.deepStrictEqual(response.headers.get('content-type'), 'text/plain') t.assert.deepStrictEqual(await response.text(), '/path/does/not/.exist.html Not Found') }) }) test('serving disabled', async (t) => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static/', serve: false } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) fastify.get('/foo/bar', (_request, reply) => { reply.sendFile('index.html') }) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html not found', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) }) await t.test('/static/index.html via sendFile found', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) }) test('sendFile', async (t) => { t.plan(4) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static' } const fastify = Fastify() const maxAge = Math.round(Math.random() * 10) * 10000 fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) fastify.get('/foo/bar', function (_req, reply) { reply.sendFile('/index.html') }) fastify.get('/root/path/override/test', (_request, reply) => { reply.sendFile( '/foo.html', path.join(__dirname, 'static', 'deep', 'path', 'for', 'test', 'purpose') ) }) fastify.get('/foo/bar/options/override/test', function (_req, reply) { reply.sendFile('/index.html', { maxAge }) }) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('reply.sendFile()', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('reply.sendFile() with rootPath', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/root/path/override/test') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) await t.test('reply.sendFile() again without root path', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('reply.sendFile() with options', async (t) => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/options/override/test') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) t.assert.deepStrictEqual(response.headers.get('cache-control'), `public, max-age=${maxAge / 1000}`) genericResponseChecks(t, response) }) }) test('sendFile disabled', async (t) => { t.plan(1) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static', decorateReply: false } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) fastify.get('/foo/bar', function (_req, reply) { if (reply.sendFile === undefined) { reply.send('pass') } else { reply.send('fail') } }) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('reply.sendFile undefined', async (t) => { t.plan(3) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), 'pass') }) }) test('allowedPath option - pathname', async (t) => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), allowedPath: (pathName) => pathName !== '/foobar.html' } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/foobar.html not found', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foobar.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/index.css found', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.css') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) }) }) test('allowedPath option - request', async (t) => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), allowedPath: (_pathName, _root, request) => request.query.key === 'temporaryKey' } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/foobar.html not found', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foobar.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/index.css found', async (t) => { t.plan(2) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.css?key=temporaryKey') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) }) }) test('download', async (t) => { t.plan(6) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static' } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) fastify.get('/foo/bar', function (_req, reply) { reply.download('/index.html') }) fastify.get('/foo/bar/change', function (_req, reply) { reply.download('/index.html', 'hello-world.html') }) fastify.get('/foo/bar/override', function (_req, reply) { reply.download('/index.html', 'hello-world.html', { maxAge: '2 hours', immutable: true }) }) fastify.get('/foo/bar/override/2', function (_req, reply) { reply.download('/index.html', { acceptRanges: false }) }) fastify.get('/root/path/override/test', (_request, reply) => { reply.download('/foo.html', { root: path.join( __dirname, 'static', 'deep', 'path', 'for', 'test', 'purpose' ) }) }) fastify.get('/root/path/override/test/change', (_request, reply) => { reply.download('/foo.html', 'hello-world.html', { root: path.join( __dirname, 'static', 'deep', 'path', 'for', 'test', 'purpose' ) }) }) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('reply.download()', async (t) => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.headers.get('content-disposition'), 'attachment; filename="index.html"') t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('reply.download() with fileName', async t => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/change') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-disposition'), 'attachment; filename="hello-world.html"') t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('reply.download() with fileName - override', async (t) => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/root/path/override/test') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-disposition'), 'attachment; filename="foo.html"') t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) await t.test('reply.download() with custom opts', async (t) => { t.plan(5 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/override') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-disposition'), 'attachment; filename="hello-world.html"') t.assert.deepStrictEqual(response.headers.get('cache-control'), 'public, max-age=7200, immutable') t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('reply.download() with custom opts (2)', async (t) => { t.plan(5 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/override/2') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-disposition'), 'attachment; filename="index.html"') t.assert.deepStrictEqual(response.headers.get('accept-ranges'), null) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('reply.download() with rootPath and fileName', async (t) => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/root/path/override/test/change') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-disposition'), 'attachment; filename="hello-world.html"') t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) }) test('download disabled', async (t) => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static', decorateReply: false } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) fastify.get('/foo/bar', function (_req, reply) { if (reply.download === undefined) { t.assert.deepStrictEqual(reply.download, undefined) reply.send('pass') } else { reply.send('fail') } }) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('reply.sendFile undefined', async (t) => { t.plan(3) const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), 'pass') }) }) test('prefix default', (t) => { t.plan(1) const pluginOptions = { root: path.join(__dirname, 'static') } const fastify = Fastify({ logger: false }) t.assert.doesNotThrow(() => fastify.register(fastifyStatic, pluginOptions)) }) test('root not found warning', async (t) => { t.plan(1) const rootPath = path.join(__dirname, 'does-not-exist') const pluginOptions = { root: rootPath } const destination = concat((data) => { t.assert.deepStrictEqual(JSON.parse(data).msg, `"root" path "${rootPath}" must exist`) }) const loggerInstance = pino( { level: 'warn' }, destination ) const fastify = Fastify({ loggerInstance }) fastify.register(fastifyStatic, pluginOptions) await fastify.listen({ port: 0 }) fastify.server.unref() destination.end() }) test('send options', (t) => { t.plan(12) const pluginOptions = { root: path.join(__dirname, '/static'), acceptRanges: 'acceptRanges', contentType: 'contentType', cacheControl: 'cacheControl', dotfiles: 'dotfiles', etag: 'etag', extensions: 'extensions', immutable: 'immutable', index: 'index', lastModified: 'lastModified', maxAge: 'maxAge' } const fastify = Fastify({ logger: false }) const { resolve, promise } = Promise.withResolvers() const fastifyStatic = require('proxyquire')('../', { '@fastify/send': function sendStub (_req, pathName, options) { t.assert.deepStrictEqual(pathName, '/index.html') t.assert.deepStrictEqual(options.root, path.join(__dirname, '/static')) t.assert.deepStrictEqual(options.acceptRanges, 'acceptRanges') t.assert.deepStrictEqual(options.contentType, 'contentType') t.assert.deepStrictEqual(options.cacheControl, 'cacheControl') t.assert.deepStrictEqual(options.dotfiles, 'dotfiles') t.assert.deepStrictEqual(options.etag, 'etag') t.assert.deepStrictEqual(options.extensions, 'extensions') t.assert.deepStrictEqual(options.immutable, 'immutable') t.assert.deepStrictEqual(options.index, 'index') t.assert.deepStrictEqual(options.lastModified, 'lastModified') t.assert.deepStrictEqual(options.maxAge, 'maxAge') resolve() return { on: () => { }, pipe: () => { } } } }) fastify.register(fastifyStatic, pluginOptions) fastify.inject({ url: '/index.html' }) return promise }) test('setHeaders option', async (t) => { t.plan(5 + GENERIC_RESPONSE_CHECK_COUNT) const pluginOptions = { root: path.join(__dirname, 'static'), setHeaders: function (res, pathName) { t.assert.deepStrictEqual(pathName, path.join(__dirname, 'static/index.html')) res.setHeader('X-Test-Header', 'test') } } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('x-test-header'), 'test') t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) test('maxAge option', async (t) => { t.plan(4 + GENERIC_RESPONSE_CHECK_COUNT) const pluginOptions = { root: path.join(__dirname, 'static'), maxAge: 3600000 } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('cache-control'), 'public, max-age=3600') t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) test('errors', async (t) => { t.plan(11) await t.test('no root', async (t) => { t.plan(1) const pluginOptions = {} const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root is not a string', async (t) => { t.plan(1) const pluginOptions = { root: 42 } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root is not an absolute path', async (t) => { t.plan(1) const pluginOptions = { root: './my/path' } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root is not a directory', async (t) => { t.plan(1) const pluginOptions = { root: __filename } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root is an empty array', async (t) => { t.plan(1) const pluginOptions = { root: [] } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root array does not contain strings', async (t) => { t.plan(1) const pluginOptions = { root: [1] } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root array does not contain an absolute path', async (t) => { t.plan(1) const pluginOptions = { root: ['./my/path'] } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('root array path is not a directory', async (t) => { t.plan(1) const pluginOptions = { root: [__filename] } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('all root array paths must be valid', async (t) => { t.plan(1) const pluginOptions = { root: [path.join(__dirname, '/static'), 1] } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('duplicate root paths are not allowed', async (t) => { t.plan(1) const pluginOptions = { root: [path.join(__dirname, '/static'), path.join(__dirname, '/static')] } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) await t.test('setHeaders is not a function', async (t) => { t.plan(1) const pluginOptions = { root: __dirname, setHeaders: 'headers' } const fastify = Fastify({ logger: false }) await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions)) }) }) test('register no prefix', async (t) => { t.plan(7) const pluginOptions = { root: path.join(__dirname, '/static') } const fastify = Fastify() fastify.register(fastifyStatic, pluginOptions) fastify.get('/', (_request, reply) => { reply.send({ hello: 'world' }) }) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), indexContent) genericResponseChecks(t, response) }) await t.test('/index.css', async (t) => { t.plan(2 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.css') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) genericResponseChecks(t, response) }) await t.test('/', async (t) => { t.plan(3) const response = await fetch('http://localhost:' + fastify.server.address().port) t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.json(), { hello: 'world' }) }) await t.test('/deep/path/for/test/purpose/foo.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/deep/path/for/test/purpose/foo.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), deepContent) genericResponseChecks(t, response) }) await t.test('/deep/path/for/test/', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/deep/path/for/test/') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(await response.text(), innerIndex) genericResponseChecks(t, response) }) await t.test('/this/path/doesnt/exist.html', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/this/path/doesnt/exist.html') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) await t.test('/../index.js', async (t) => { t.plan(2 + GENERIC_ERROR_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/../index.js') t.assert.ok(!response.ok) t.assert.deepStrictEqual(response.status, 404) genericErrorResponseChecks(t, response) }) }) test('with fastify-compress', async t => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static') } const fastify = Fastify() fastify.register(compress, { threshold: 0 }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) await t.test('deflate', async function (t) { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html', { headers: { 'accept-encoding': ['deflate'] } }) t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-encoding'), 'deflate') genericResponseChecks(t, response) }) await t.test('gzip', async function (t) { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-encoding'), 'gzip') genericResponseChecks(t, response) }) }) test('register /static/ with schemaHide true', async t => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static/', schemaHide: true } const fastify = Fastify() fastify.addHook('onRoute', function (routeOptions) { t.assert.deepStrictEqual(routeOptions.schema, { hide: true }) }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-type'), 'text/html; charset=utf-8') genericResponseChecks(t, response) }) }) test('register /static/ with schemaHide false', async t => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static/', schemaHide: false } const fastify = Fastify() fastify.addHook('onRoute', function (routeOptions) { t.assert.deepStrictEqual(routeOptions.schema, { hide: false }) }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(response.ok) t.assert.deepStrictEqual(response.status, 200) t.assert.deepStrictEqual(response.headers.get('content-type'), 'text/html; charset=utf-8') genericResponseChecks(t, response) }) }) test('register /static/ without schemaHide', async t => { t.plan(2) const pluginOptions = { root: path.join(__dirname, '/static'), prefix: '/static/' } const fastify = Fastify() fastify.addHook('onRoute', function (routeOptions) { t.assert.deepStrictEqual(routeOptions.schema, { hide: true }) }) fastify.register(fastifyStatic, pluginOptions) t.after(() => fastify.close()) await fastify.listen({ port: 0 }) fastify.server.unref() await t.test('/static/index.html', async (t) => { t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT) const response = await fetch('http://localhost:' + fastify.server.address().port + '/static/index.html') t.assert.ok(