UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

2,151 lines (1,796 loc) 85.3 kB
'use strict' const t = require('tap') const test = t.test const sget = require('simple-get').concat const stream = require('node:stream') const Fastify = require('..') const fp = require('fastify-plugin') const fs = require('node:fs') const split = require('split2') const symbols = require('../lib/symbols.js') const payload = { hello: 'world' } const proxyquire = require('proxyquire') const { connect } = require('node:net') const { sleep, getServerUrl } = require('./helper') process.removeAllListeners('warning') test('hooks', t => { t.plan(49) const fastify = Fastify({ exposeHeadRoutes: false }) try { fastify.addHook('preHandler', function (request, reply, done) { t.equal(request.test, 'the request is coming') t.equal(reply.test, 'the reply has come') if (request.raw.method === 'HEAD') { done(new Error('some error')) } else { done() } }) t.pass() } catch (e) { t.fail() } try { fastify.addHook('preHandler', null) } catch (e) { t.equal(e.code, 'FST_ERR_HOOK_INVALID_HANDLER') t.equal(e.message, 'preHandler hook should be a function, instead got null') t.pass() } try { fastify.addHook('preParsing') } catch (e) { t.equal(e.code, 'FST_ERR_HOOK_INVALID_HANDLER') t.equal(e.message, 'preParsing hook should be a function, instead got undefined') t.pass() } try { fastify.addHook('preParsing', function (request, reply, payload, done) { request.preParsing = true t.equal(request.test, 'the request is coming') t.equal(reply.test, 'the reply has come') done() }) t.pass() } catch (e) { t.fail() } try { fastify.addHook('preParsing', function (request, reply, payload, done) { request.preParsing = true t.equal(request.test, 'the request is coming') t.equal(reply.test, 'the reply has come') done() }) t.pass() } catch (e) { t.fail() } try { fastify.addHook('preValidation', function (request, reply, done) { t.equal(request.preParsing, true) t.equal(request.test, 'the request is coming') t.equal(reply.test, 'the reply has come') done() }) t.pass() } catch (e) { t.fail() } try { fastify.addHook('preSerialization', function (request, reply, payload, done) { t.ok('preSerialization called') done() }) t.pass() } catch (e) { t.fail() } try { fastify.addHook('onRequest', function (request, reply, done) { request.test = 'the request is coming' reply.test = 'the reply has come' if (request.raw.method === 'DELETE') { done(new Error('some error')) } else { done() } }) t.pass() } catch (e) { t.fail() } fastify.addHook('onResponse', function (request, reply, done) { t.ok('onResponse called') done() }) fastify.addHook('onSend', function (req, reply, thePayload, done) { t.ok('onSend called') done() }) fastify.route({ method: 'GET', url: '/', handler: function (req, reply) { t.equal(req.test, 'the request is coming') t.equal(reply.test, 'the reply has come') reply.code(200).send(payload) }, onResponse: function (req, reply, done) { t.ok('onResponse inside hook') }, response: { 200: { type: 'object' } } }) fastify.head('/', function (req, reply) { reply.code(200).send(payload) }) fastify.delete('/', function (req, reply) { reply.code(200).send(payload) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'HEAD', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 500) }) sget({ method: 'DELETE', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 500) }) }) }) test('onRequest hook should support encapsulation / 1', t => { t.plan(5) const fastify = Fastify() fastify.register((instance, opts, done) => { instance.addHook('onRequest', (req, reply, done) => { t.equal(req.raw.url, '/plugin') done() }) instance.get('/plugin', (request, reply) => { reply.send() }) done() }) fastify.get('/root', (request, reply) => { reply.send() }) fastify.inject('/root', (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) fastify.inject('/plugin', (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) }) test('onRequest hook should support encapsulation / 2', t => { t.plan(3) const fastify = Fastify() let pluginInstance fastify.addHook('onRequest', () => {}) fastify.register((instance, opts, done) => { instance.addHook('onRequest', () => {}) pluginInstance = instance done() }) fastify.ready(err => { t.error(err) t.equal(fastify[symbols.kHooks].onRequest.length, 1) t.equal(pluginInstance[symbols.kHooks].onRequest.length, 2) }) }) test('onRequest hook should support encapsulation / 3', t => { t.plan(20) const fastify = Fastify() fastify.decorate('hello', 'world') fastify.addHook('onRequest', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) req.first = true done() }) fastify.decorate('hello2', 'world') fastify.get('/first', (req, reply) => { t.ok(req.first) t.notOk(req.second) reply.send({ hello: 'world' }) }) fastify.register((instance, opts, done) => { instance.decorate('hello3', 'world') instance.addHook('onRequest', function (req, reply, done) { t.ok(this.hello) t.ok(this.hello2) t.ok(this.hello3) req.second = true done() }) instance.get('/second', (req, reply) => { t.ok(req.first) t.ok(req.second) reply.send({ hello: 'world' }) }) done() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/first' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/second' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) }) }) test('preHandler hook should support encapsulation / 5', t => { t.plan(17) const fastify = Fastify() fastify.decorate('hello', 'world') fastify.addHook('preHandler', function (req, res, done) { t.ok(this.hello) req.first = true done() }) fastify.get('/first', (req, reply) => { t.ok(req.first) t.notOk(req.second) reply.send({ hello: 'world' }) }) fastify.register((instance, opts, done) => { instance.decorate('hello2', 'world') instance.addHook('preHandler', function (req, res, done) { t.ok(this.hello) t.ok(this.hello2) req.second = true done() }) instance.get('/second', (req, reply) => { t.ok(req.first) t.ok(req.second) reply.send({ hello: 'world' }) }) done() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/first' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/second' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) }) }) test('onRoute hook should be called / 1', t => { t.plan(2) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', () => { t.pass() }) instance.get('/', opts, function (req, reply) { reply.send() }) done() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should be called / 2', t => { t.plan(5) let firstHandler = 0 let secondHandler = 0 const fastify = Fastify({ exposeHeadRoutes: false }) fastify.addHook('onRoute', (route) => { t.pass() firstHandler++ }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.pass() secondHandler++ }) instance.get('/', opts, function (req, reply) { reply.send() }) done() }) .after(() => { t.equal(firstHandler, 1) t.equal(secondHandler, 1) }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should be called / 3', t => { t.plan(5) const fastify = Fastify({ exposeHeadRoutes: false }) function handler (req, reply) { reply.send() } fastify.addHook('onRoute', (route) => { t.pass() }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.pass() }) instance.get('/a', handler) done() }) .after((err, done) => { t.error(err) setTimeout(() => { fastify.get('/b', handler) done() }, 10) }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should be called (encapsulation support) / 4', t => { t.plan(4) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.addHook('onRoute', () => { t.pass() }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', () => { t.pass() }) instance.get('/nested', opts, function (req, reply) { reply.send() }) done() }) fastify.get('/', function (req, reply) { reply.send() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should be called (encapsulation support) / 5', t => { t.plan(2) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.get('/first', function (req, reply) { reply.send() }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', () => { t.pass() }) instance.get('/nested', opts, function (req, reply) { reply.send() }) done() }) fastify.get('/second', function (req, reply) { reply.send() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should be called (encapsulation support) / 6', t => { t.plan(1) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.get('/first', function (req, reply) { reply.send() }) fastify.addHook('onRoute', () => { t.fail('This should not be called') }) fastify.ready(err => { t.error(err) }) }) test('onRoute should keep the context', t => { t.plan(4) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.decorate('test', true) instance.addHook('onRoute', onRoute) t.ok(instance.prototype === fastify.prototype) function onRoute (route) { t.ok(this.test) t.equal(this, instance) } instance.get('/', opts, function (req, reply) { reply.send() }) done() }) fastify.close((err) => { t.error(err) }) }) test('onRoute hook should pass correct route', t => { t.plan(9) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.addHook('onRoute', (route) => { t.equal(route.method, 'GET') t.equal(route.url, '/') t.equal(route.path, '/') t.equal(route.routePath, '/') }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.equal(route.method, 'GET') t.equal(route.url, '/') t.equal(route.path, '/') t.equal(route.routePath, '/') }) instance.get('/', opts, function (req, reply) { reply.send() }) done() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should pass correct route with custom prefix', t => { t.plan(11) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.addHook('onRoute', function (route) { t.equal(route.method, 'GET') t.equal(route.url, '/v1/foo') t.equal(route.path, '/v1/foo') t.equal(route.routePath, '/foo') t.equal(route.prefix, '/v1') }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.equal(route.method, 'GET') t.equal(route.url, '/v1/foo') t.equal(route.path, '/v1/foo') t.equal(route.routePath, '/foo') t.equal(route.prefix, '/v1') }) instance.get('/foo', opts, function (req, reply) { reply.send() }) done() }, { prefix: '/v1' }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should pass correct route with custom options', t => { t.plan(6) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.equal(route.method, 'GET') t.equal(route.url, '/foo') t.equal(route.logLevel, 'info') t.equal(route.bodyLimit, 100) t.type(route.logSerializers.test, 'function') }) instance.get('/foo', { logLevel: 'info', bodyLimit: 100, logSerializers: { test: value => value } }, function (req, reply) { reply.send() }) done() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should receive any route option', t => { t.plan(5) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.equal(route.method, 'GET') t.equal(route.url, '/foo') t.equal(route.routePath, '/foo') t.equal(route.auth, 'basic') }) instance.get('/foo', { auth: 'basic' }, function (req, reply) { reply.send() }) done() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should preserve system route configuration', t => { t.plan(5) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.equal(route.method, 'GET') t.equal(route.url, '/foo') t.equal(route.routePath, '/foo') t.equal(route.handler.length, 2) }) instance.get('/foo', { url: '/bar', method: 'POST' }, function (req, reply) { reply.send() }) done() }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should preserve handler function in options of shorthand route system configuration', t => { t.plan(2) const handler = (req, reply) => {} const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', function (route) { t.equal(route.handler, handler) }) instance.get('/foo', { handler }) done() }) fastify.ready(err => { t.error(err) }) }) // issue ref https://github.com/fastify/fastify-compress/issues/140 test('onRoute hook should be called once when prefixTrailingSlash', t => { t.plan(3) let onRouteCalled = 0 let routePatched = 0 const fastify = Fastify({ ignoreTrailingSlash: false, exposeHeadRoutes: false }) // a plugin that patches route options, similar to fastify-compress fastify.register(fp(function myPlugin (instance, opts, next) { function patchTheRoute () { routePatched++ } instance.addHook('onRoute', function (routeOptions) { onRouteCalled++ patchTheRoute(routeOptions) }) next() })) fastify.register(function routes (instance, opts, next) { instance.route({ method: 'GET', url: '/', prefixTrailingSlash: 'both', handler: (req, reply) => { reply.send({ hello: 'world' }) } }) next() }, { prefix: '/prefix' }) fastify.ready(err => { t.error(err) t.equal(onRouteCalled, 1) // onRoute hook was called once t.equal(routePatched, 1) // and plugin acted once and avoided redundant route patching }) }) test('onRoute hook should able to change the route url', t => { t.plan(5) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.equal(route.url, '/foo') route.url = encodeURI(route.url) }) instance.get('/foo', (request, reply) => { reply.send('here /foo') }) done() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: getServerUrl(fastify) + encodeURI('/foo') }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(body.toString(), 'here /foo') }) }) }) test('onRoute hook that throws should be caught', t => { t.plan(1) const fastify = Fastify({ exposeHeadRoutes: false }) fastify.register((instance, opts, done) => { instance.addHook('onRoute', () => { throw new Error('snap') }) try { instance.get('/', opts, function (req, reply) { reply.send() }) t.fail('onRoute should throw sync if error') } catch (error) { t.ok(error) } done() }) fastify.ready() }) test('onRoute hook with many prefix', t => { t.plan(3) const fastify = Fastify({ exposeHeadRoutes: false }) const handler = (req, reply) => { reply.send({}) } const onRouteChecks = [ { routePath: '/anotherPath', prefix: '/two', url: '/one/two/anotherPath' }, { routePath: '/aPath', prefix: '/one', url: '/one/aPath' } ] fastify.register((instance, opts, done) => { instance.addHook('onRoute', (route) => { t.match(route, onRouteChecks.pop()) }) instance.route({ method: 'GET', url: '/aPath', handler }) instance.register((instance, opts, done) => { instance.route({ method: 'GET', path: '/anotherPath', handler }) done() }, { prefix: '/two' }) done() }, { prefix: '/one' }) fastify.ready(err => { t.error(err) }) }) test('onRoute hook should not be called when it registered after route', t => { t.plan(3) const fastify = Fastify() fastify.addHook('onRoute', () => { t.pass() }) fastify.get('/', function (req, reply) { reply.send() }) fastify.addHook('onRoute', () => { t.fail('should not be called') }) fastify.ready(err => { t.error(err) }) }) test('onResponse hook should log request error', t => { t.plan(4) let fastify = null const logStream = split(JSON.parse) try { fastify = Fastify({ logger: { stream: logStream, level: 'error' } }) } catch (e) { t.fail() } logStream.once('data', line => { t.equal(line.msg, 'request errored') t.equal(line.level, 50) }) fastify.addHook('onResponse', (request, reply, done) => { done(new Error('kaboom')) }) fastify.get('/root', (request, reply) => { reply.send() }) fastify.inject('/root', (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) }) test('onResponse hook should support encapsulation / 1', t => { t.plan(5) const fastify = Fastify() fastify.register((instance, opts, done) => { instance.addHook('onResponse', (request, reply, done) => { t.equal(reply.plugin, true) done() }) instance.get('/plugin', (request, reply) => { reply.plugin = true reply.send() }) done() }) fastify.get('/root', (request, reply) => { reply.send() }) fastify.inject('/root', (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) fastify.inject('/plugin', (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) }) test('onResponse hook should support encapsulation / 2', t => { t.plan(3) const fastify = Fastify() let pluginInstance fastify.addHook('onResponse', () => {}) fastify.register((instance, opts, done) => { instance.addHook('onResponse', () => {}) pluginInstance = instance done() }) fastify.ready(err => { t.error(err) t.equal(fastify[symbols.kHooks].onResponse.length, 1) t.equal(pluginInstance[symbols.kHooks].onResponse.length, 2) }) }) test('onResponse hook should support encapsulation / 3', t => { t.plan(16) const fastify = Fastify() fastify.decorate('hello', 'world') fastify.addHook('onResponse', function (request, reply, done) { t.ok(this.hello) t.ok('onResponse called') done() }) fastify.get('/first', (req, reply) => { reply.send({ hello: 'world' }) }) fastify.register((instance, opts, done) => { instance.decorate('hello2', 'world') instance.addHook('onResponse', function (request, reply, done) { t.ok(this.hello) t.ok(this.hello2) t.ok('onResponse called') done() }) instance.get('/second', (req, reply) => { reply.send({ hello: 'world' }) }) done() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/first' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/second' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) }) }) test('onSend hook should support encapsulation / 1', t => { t.plan(3) const fastify = Fastify() let pluginInstance fastify.addHook('onSend', () => {}) fastify.register((instance, opts, done) => { instance.addHook('onSend', () => {}) pluginInstance = instance done() }) fastify.ready(err => { t.error(err) t.equal(fastify[symbols.kHooks].onSend.length, 1) t.equal(pluginInstance[symbols.kHooks].onSend.length, 2) }) }) test('onSend hook should support encapsulation / 2', t => { t.plan(16) const fastify = Fastify() fastify.decorate('hello', 'world') fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok(this.hello) t.ok('onSend called') done() }) fastify.get('/first', (req, reply) => { reply.send({ hello: 'world' }) }) fastify.register((instance, opts, done) => { instance.decorate('hello2', 'world') instance.addHook('onSend', function (request, reply, thePayload, done) { t.ok(this.hello) t.ok(this.hello2) t.ok('onSend called') done() }) instance.get('/second', (req, reply) => { reply.send({ hello: 'world' }) }) done() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/first' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/second' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) }) }) test('onSend hook is called after payload is serialized and headers are set', t => { t.plan(30) const fastify = Fastify() fastify.register((instance, opts, done) => { const thePayload = { hello: 'world' } instance.addHook('onSend', function (request, reply, payload, done) { t.same(JSON.parse(payload), thePayload) t.equal(reply[symbols.kReplyHeaders]['content-type'], 'application/json; charset=utf-8') done() }) instance.get('/json', (request, reply) => { reply.send(thePayload) }) done() }) fastify.register((instance, opts, done) => { instance.addHook('onSend', function (request, reply, payload, done) { t.equal(payload, 'some text') t.equal(reply[symbols.kReplyHeaders]['content-type'], 'text/plain; charset=utf-8') done() }) instance.get('/text', (request, reply) => { reply.send('some text') }) done() }) fastify.register((instance, opts, done) => { const thePayload = Buffer.from('buffer payload') instance.addHook('onSend', function (request, reply, payload, done) { t.equal(payload, thePayload) t.equal(reply[symbols.kReplyHeaders]['content-type'], 'application/octet-stream') done() }) instance.get('/buffer', (request, reply) => { reply.send(thePayload) }) done() }) fastify.register((instance, opts, done) => { let chunk = 'stream payload' const thePayload = new stream.Readable({ read () { this.push(chunk) chunk = null } }) instance.addHook('onSend', function (request, reply, payload, done) { t.equal(payload, thePayload) t.equal(reply[symbols.kReplyHeaders]['content-type'], 'application/octet-stream') done() }) instance.get('/stream', (request, reply) => { reply.header('content-type', 'application/octet-stream') reply.send(thePayload) }) done() }) fastify.register((instance, opts, done) => { const serializedPayload = 'serialized' instance.addHook('onSend', function (request, reply, payload, done) { t.equal(payload, serializedPayload) t.equal(reply[symbols.kReplyHeaders]['content-type'], 'text/custom') done() }) instance.get('/custom-serializer', (request, reply) => { reply .serializer(() => serializedPayload) .type('text/custom') .send('needs to be serialized') }) done() }) fastify.inject({ method: 'GET', url: '/json' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(JSON.parse(res.payload), { hello: 'world' }) t.equal(res.headers['content-length'], '17') }) fastify.inject({ method: 'GET', url: '/text' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(res.payload, 'some text') t.equal(res.headers['content-length'], '9') }) fastify.inject({ method: 'GET', url: '/buffer' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(res.payload, 'buffer payload') t.equal(res.headers['content-length'], '14') }) fastify.inject({ method: 'GET', url: '/stream' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(res.payload, 'stream payload') t.equal(res.headers['transfer-encoding'], 'chunked') }) fastify.inject({ method: 'GET', url: '/custom-serializer' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(res.payload, 'serialized') t.equal(res.headers['content-type'], 'text/custom') }) }) test('modify payload', t => { t.plan(10) const fastify = Fastify() const payload = { hello: 'world' } const modifiedPayload = { hello: 'modified' } const anotherPayload = '"winter is coming"' fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok('onSend called') t.same(JSON.parse(thePayload), payload) thePayload = thePayload.replace('world', 'modified') done(null, thePayload) }) fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok('onSend called') t.same(JSON.parse(thePayload), modifiedPayload) done(null, anotherPayload) }) fastify.addHook('onSend', function (request, reply, thePayload, done) { t.ok('onSend called') t.equal(thePayload, anotherPayload) done() }) fastify.get('/', (req, reply) => { reply.send(payload) }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, anotherPayload) t.equal(res.statusCode, 200) t.equal(res.headers['content-length'], '18') }) }) test('clear payload', t => { t.plan(6) const fastify = Fastify() fastify.addHook('onSend', function (request, reply, payload, done) { t.ok('onSend called') reply.code(304) done(null, null) }) fastify.get('/', (req, reply) => { reply.send({ hello: 'world' }) }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.statusCode, 304) t.equal(res.payload, '') t.equal(res.headers['content-length'], undefined) t.equal(res.headers['content-type'], 'application/json; charset=utf-8') }) }) test('onSend hook throws', t => { t.plan(11) const Fastify = proxyquire('..', { './lib/schemas.js': { getSchemaSerializer: (param1, param2, param3) => { t.equal(param3, 'application/json; charset=utf-8', 'param3 should be "application/json; charset=utf-8"') } } }) const fastify = Fastify() fastify.addHook('onSend', function (request, reply, payload, done) { if (request.raw.method === 'DELETE') { done(new Error('some error')) return } if (request.raw.method === 'PUT') { throw new Error('some error') } if (request.raw.method === 'POST') { throw new Error('some error') } done() }) fastify.get('/', (req, reply) => { reply.send({ hello: 'world' }) }) fastify.post('/', { schema: { response: { 200: { content: { 'application/json': { schema: { name: { type: 'string' }, image: { type: 'string' }, address: { type: 'string' } } } } } } } }, (req, reply) => { reply.send({ hello: 'world' }) }) fastify.delete('/', (req, reply) => { reply.send({ hello: 'world' }) }) fastify.put('/', (req, reply) => { reply.send({ hello: 'world' }) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(() => { fastify.close() }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-length'], '' + body.length) t.same(JSON.parse(body), { hello: 'world' }) }) sget({ method: 'POST', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 500) }) sget({ method: 'DELETE', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 500) }) sget({ method: 'PUT', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 500) }) }) }) test('onSend hook should receive valid request and reply objects if onRequest hook fails', t => { t.plan(4) const fastify = Fastify() fastify.decorateRequest('testDecorator', 'testDecoratorVal') fastify.decorateReply('testDecorator', 'testDecoratorVal') fastify.addHook('onRequest', function (req, reply, done) { done(new Error('onRequest hook failed')) }) fastify.addHook('onSend', function (request, reply, payload, done) { t.equal(request.testDecorator, 'testDecoratorVal') t.equal(reply.testDecorator, 'testDecoratorVal') done() }) fastify.get('/', (req, reply) => { reply.send('hello') }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.statusCode, 500) }) }) test('onSend hook should receive valid request and reply objects if a custom content type parser fails', t => { t.plan(4) const fastify = Fastify() fastify.decorateRequest('testDecorator', 'testDecoratorVal') fastify.decorateReply('testDecorator', 'testDecoratorVal') fastify.addContentTypeParser('*', function (req, payload, done) { done(new Error('content type parser failed')) }) fastify.addHook('onSend', function (request, reply, payload, done) { t.equal(request.testDecorator, 'testDecoratorVal') t.equal(reply.testDecorator, 'testDecoratorVal') done() }) fastify.get('/', (req, reply) => { reply.send('hello') }) fastify.inject({ method: 'POST', url: '/', payload: 'body' }, (err, res) => { t.error(err) t.equal(res.statusCode, 500) }) }) test('Content-Length header should be updated if onSend hook modifies the payload', t => { t.plan(2) const instance = Fastify() instance.get('/', async (_, rep) => { rep.header('content-length', 3) return 'foo' }) instance.addHook('onSend', async () => 'bar12233000') instance.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) const payloadLength = Buffer.byteLength(res.body) const contentLength = Number(res.headers['content-length']) t.equal(payloadLength, contentLength) }) }) test('cannot add hook after binding', t => { t.plan(2) const instance = Fastify() instance.get('/', function (request, reply) { reply.send({ hello: 'world' }) }) instance.listen({ port: 0 }, err => { t.error(err) t.teardown(instance.server.close.bind(instance.server)) try { instance.addHook('onRequest', () => {}) t.fail() } catch (e) { t.pass() } }) }) test('onRequest hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() fastify.addHook('onRequest', (req, reply, done) => { reply.send('hello') done() }) fastify.addHook('onRequest', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('preValidation hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() fastify.addHook('preValidation', (req, reply, done) => { reply.send('hello') done() }) fastify.addHook('preValidation', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('preValidation hooks should be able to change request body before validation', t => { t.plan(4) const fastify = Fastify() fastify.addHook('preValidation', (req, _reply, done) => { const buff = Buffer.from(req.body.message, 'base64') req.body = JSON.parse(buff.toString('utf-8')) done() }) fastify.post( '/', { schema: { body: { type: 'object', properties: { foo: { type: 'string' }, bar: { type: 'number' } }, required: ['foo', 'bar'] } } }, (req, reply) => { t.pass() reply.status(200).send('hello') } ) fastify.inject({ url: '/', method: 'POST', payload: { message: Buffer.from(JSON.stringify({ foo: 'example', bar: 1 })).toString('base64') } }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('preParsing hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() fastify.addHook('preParsing', (req, reply, payload, done) => { reply.send('hello') done() }) fastify.addHook('preParsing', (req, reply, payload, done) => { t.fail('this should not be called') }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('preHandler hooks should be able to block a request', t => { t.plan(5) const fastify = Fastify() fastify.addHook('preHandler', (req, reply, done) => { reply.send('hello') done() }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.equal(payload, 'hello') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('onRequest hooks should be able to block a request (last hook)', t => { t.plan(5) const fastify = Fastify() fastify.addHook('onRequest', (req, reply, done) => { reply.send('hello') done() }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('preHandler hooks should be able to block a request (last hook)', t => { t.plan(5) const fastify = Fastify() fastify.addHook('preHandler', (req, reply, done) => { reply.send('hello') done() }) fastify.addHook('onSend', (req, reply, payload, done) => { t.equal(payload, 'hello') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.equal(res.payload, 'hello') }) }) test('preParsing hooks should handle errors', t => { t.plan(3) const fastify = Fastify() fastify.addHook('preParsing', (req, reply, payload, done) => { const e = new Error('kaboom') e.statusCode = 501 throw e }) fastify.post('/', function (request, reply) { reply.send(request.body) }) fastify.inject({ method: 'POST', url: '/', payload: { hello: 'world' } }, (err, res) => { t.error(err) t.equal(res.statusCode, 501) t.same(JSON.parse(res.payload), { error: 'Not Implemented', message: 'kaboom', statusCode: 501 }) }) }) test('onRequest respond with a stream', t => { t.plan(4) const fastify = Fastify() fastify.addHook('onRequest', (req, reply, done) => { const stream = fs.createReadStream(__filename, 'utf8') // stream.pipe(res) // res.once('finish', done) reply.send(stream) }) fastify.addHook('onRequest', (req, res, done) => { t.fail('this should not be called') }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.ok('called') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) }) test('preHandler respond with a stream', t => { t.plan(7) const fastify = Fastify() fastify.addHook('onRequest', (req, reply, done) => { t.ok('called') done() }) // we are calling `reply.send` inside the `preHandler` hook with a stream, // this triggers the `onSend` hook event if `preHandler` has not yet finished const order = [1, 2] fastify.addHook('preHandler', (req, reply, done) => { const stream = fs.createReadStream(__filename, 'utf8') reply.send(stream) reply.raw.once('finish', () => { t.equal(order.shift(), 2) done() }) }) fastify.addHook('preHandler', (req, reply, done) => { t.fail('this should not be called') }) fastify.addHook('onSend', (req, reply, payload, done) => { t.equal(order.shift(), 1) t.equal(typeof payload.pipe, 'function') done() }) fastify.addHook('onResponse', (request, reply, done) => { t.ok('called') done() }) fastify.get('/', function (request, reply) { t.fail('we should not be here') }) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) }) }) test('Register an hook after a plugin inside a plugin', t => { t.plan(6) const fastify = Fastify() fastify.register(fp(function (instance, opts, done) { instance.addHook('preHandler', function (req, reply, done) { t.ok('called') done() }) instance.get('/', function (request, reply) { reply.send({ hello: 'world' }) }) done() })) fastify.register(fp(function (instance, opts, done) { instance.addHook('preHandler', function (req, reply, done) { t.ok('called') done() }) instance.addHook('preHandler', function (req, reply, done) { t.ok('called') done() }) done() })) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(JSON.parse(res.payload), { hello: 'world' }) }) }) test('Register an hook after a plugin inside a plugin (with preHandler option)', t => { t.plan(7) const fastify = Fastify() fastify.register(fp(function (instance, opts, done) { instance.addHook('preHandler', function (req, reply, done) { t.ok('called') done() }) instance.get('/', { preHandler: (req, reply, done) => { t.ok('called') done() } }, function (request, reply) { reply.send({ hello: 'world' }) }) done() })) fastify.register(fp(function (instance, opts, done) { instance.addHook('preHandler', function (req, reply, done) { t.ok('called') done() }) instance.addHook('preHandler', function (req, reply, done) { t.ok('called') done() }) done() })) fastify.inject({ url: '/', method: 'GET' }, (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(JSON.parse(res.payload), { hello: 'world' }) }) }) test('Register hooks inside a plugin after an encapsulated plugin', t => { t.plan(7) const fastify = Fastify() fastify.register(function (instance, opts, done) { instance.get('/', function (request, reply) { reply.send({ hello: 'world' }) }) done() }) fastify.register(fp(function (instance, opts, done) { instance.addHook('onRequest', function (req, reply, done) { t.ok('called') done() }) instance.addHook('preHandler', function (request, reply, done) { t.ok('called') done() }) instance.addHook('onSend', function (request, reply, payload, done) { t.ok('called') done() }) instance.addHook('onResponse', function (request, reply, done) { t.ok('called') done() }) done() })) fastify.inject('/', (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(JSON.parse(res.payload), { hello: 'world' }) }) }) test('onRequest hooks should run in the order in which they are defined', t => { t.plan(9) const fastify = Fastify() fastify.register(function (instance, opts, done) { instance.addHook('onRequest', function (req, reply, done) { t.equal(req.previous, undefined) req.previous = 1 done() }) instance.get('/', function (request, reply) { t.equal(request.previous, 5) reply.send({ hello: 'world' }) }) instance.register(fp(function (i, opts, done) { i.addHook('onRequest', function (req, reply, done) { t.equal(req.previous, 1) req.previous = 2 done() }) done() })) done() }) fastify.register(fp(function (instance, opts, done) { instance.addHook('onRequest', function (req, reply, done) { t.equal(req.previous, 2) req.previous = 3 done() }) instance.register(fp(function (i, opts, done) { i.addHook('onRequest', function (req, reply, done) { t.equal(req.previous, 3) req.previous = 4 done() }) done() })) instance.addHook('onRequest', function (req, reply, done) { t.equal(req.previous, 4) req.previous = 5 done() }) done() })) fastify.inject('/', (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(JSON.parse(res.payload), { hello: 'world' }) }) }) test('preHandler hooks should run in the order in which they are defined', t => { t.plan(9) const fastify = Fastify() fastify.register(function (instance, opts, done) { instance.addHook('preHandler', function (request, reply, done) { t.equal(request.previous, undefined) request.previous = 1 done() }) instance.get('/', function (request, reply) { t.equal(request.previous, 5) reply.send({ hello: 'world' }) }) instance.register(fp(function (i, opts, done) { i.addHook('preHandler', function (request, reply, done) { t.equal(request.previous, 1) request.previous = 2 done() }) done() })) done() }) fastify.register(fp(function (instance, opts, done) { instance.addHook('preHandler', function (request, reply, done) { t.equal(request.previous, 2) request.previous = 3 done() }) instance.register(fp(function (i, opts, done) { i.addHook('preHandler', function (request, reply, done) { t.equal(request.previous, 3) request.previous = 4 done() }) done() })) instance.addHook('preHandler', function (request, reply, done) { t.equal(request.previous, 4) request.previous = 5 done() }) done() })) fastify.inject('/', (err, res) => { t.error(err) t.equal(res.statusCode, 200) t.same(JSON.parse(res.payload), { hello: 'world' }) }) }) test('onSend hooks should run in the order in which they are defined', t => { t.plan(8) const fastify = Fastify() fastify.register(function (instance, opts, done) { instance.addHook('onSend', function (request, reply, payload, done) { t.equal(request.previous, undefined) request.previous = 1 done() }) instance.get('/', function (request, reply) { reply.send({}) }) instance.register(fp(function (i, opts, done) { i.addHook('onSend', function (request, reply, payload, done) { t.equal(request.previous, 1) request.previous = 2 done() }) done() })) done() }) fastify.register(fp(function (instance, opts, done) { instance.addHook('onSend', function (request, reply, payload, done) { t.equal(request.previous, 2) request.previous = 3 done() }) instance.register(fp(function (i, opts, done) { i.addHook('onSend', function (request, reply, payload, done) { t.equal(request.previous, 3) request.previous = 4 done() }) done(