UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

1,919 lines (1,607 loc) 59 kB
'use strict' const t = require('tap') const test = t.test const sget = require('simple-get').concat const http = require('node:http') const NotFound = require('http-errors').NotFound const Reply = require('../../lib/reply') const Fastify = require('../..') const { Readable, Writable } = require('node:stream') const { kReplyErrorHandlerCalled, kReplyHeaders, kReplySerializer, kReplyIsError, kReplySerializerDefault, kRouteContext, kPublicRouteContext } = require('../../lib/symbols') const fs = require('node:fs') const path = require('node:path') const { FSTDEP010, FSTDEP019, FSTDEP020, FSTDEP021 } = require('../../lib/warnings') const agent = new http.Agent({ keepAlive: false }) const doGet = function (url) { return new Promise((resolve, reject) => { sget({ method: 'GET', url, followRedirects: false, agent }, (err, response, body) => { if (err) { reject(err) } else { resolve({ response, body }) } }) }) } test('Once called, Reply should return an object with methods', t => { t.plan(16) const response = { res: 'res' } const context = { config: { onSend: [] }, schema: {} } const request = { [kRouteContext]: context, [kPublicRouteContext]: { config: context.config, schema: context.schema } } const reply = new Reply(response, request) t.equal(typeof reply, 'object') t.equal(typeof reply[kReplyIsError], 'boolean') t.equal(typeof reply[kReplyErrorHandlerCalled], 'boolean') t.equal(typeof reply.send, 'function') t.equal(typeof reply.code, 'function') t.equal(typeof reply.status, 'function') t.equal(typeof reply.header, 'function') t.equal(typeof reply.serialize, 'function') t.equal(typeof reply.getResponseTime, 'function') t.equal(typeof reply[kReplyHeaders], 'object') t.same(reply.raw, response) t.equal(reply[kRouteContext], context) t.equal(reply[kPublicRouteContext].config, context.config) t.equal(reply[kPublicRouteContext].schema, context.schema) t.equal(reply.request, request) // Aim to not bad property keys (including Symbols) t.notOk('undefined' in reply) }) test('reply.send will logStream error and destroy the stream', t => { t.plan(1) let destroyCalled const payload = new Readable({ read () {}, destroy (err, cb) { destroyCalled = true cb(err) } }) const response = new Writable() Object.assign(response, { setHeader: () => {}, hasHeader: () => false, getHeader: () => undefined, writeHead: () => {}, write: () => {}, headersSent: true }) const log = { warn: () => {} } const reply = new Reply(response, { [kRouteContext]: { onSend: null } }, log) reply.send(payload) payload.destroy(new Error('stream error')) t.equal(destroyCalled, true, 'Error not logged and not streamed') }) test('reply.send throw with circular JSON', t => { t.plan(1) const response = { setHeader: () => {}, hasHeader: () => false, getHeader: () => undefined, writeHead: () => {}, write: () => {}, end: () => {} } const reply = new Reply(response, { [kRouteContext]: { onSend: [] } }) t.throws(() => { const obj = {} obj.obj = obj reply.send(JSON.stringify(obj)) }, 'Converting circular structure to JSON') }) test('reply.send returns itself', t => { t.plan(1) const response = { setHeader: () => {}, hasHeader: () => false, getHeader: () => undefined, writeHead: () => {}, write: () => {}, end: () => {} } const reply = new Reply(response, { [kRouteContext]: { onSend: [] } }) t.equal(reply.send('hello'), reply) }) test('reply.serializer should set a custom serializer', t => { t.plan(2) const reply = new Reply(null, null, null) t.equal(reply[kReplySerializer], null) reply.serializer('serializer') t.equal(reply[kReplySerializer], 'serializer') }) test('reply.serializer should support running preSerialization hooks', t => { t.plan(3) const fastify = Fastify() t.teardown(fastify.close.bind(fastify)) fastify.addHook('preSerialization', async (request, reply, payload) => { t.ok('called', 'preSerialization') }) fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply .type('application/json') .serializer(JSON.stringify) .send({ foo: 'bar' }) } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, '{"foo":"bar"}') }) }) test('reply.serialize should serialize payload', t => { t.plan(1) const response = { statusCode: 200 } const context = {} const reply = new Reply(response, { [kRouteContext]: context }) t.equal(reply.serialize({ foo: 'bar' }), '{"foo":"bar"}') }) test('reply.serialize should serialize payload with a custom serializer', t => { t.plan(2) let customSerializerCalled = false const response = { statusCode: 200 } const context = {} const reply = new Reply(response, { [kRouteContext]: context }) reply.serializer((x) => (customSerializerCalled = true) && JSON.stringify(x)) t.equal(reply.serialize({ foo: 'bar' }), '{"foo":"bar"}') t.equal(customSerializerCalled, true, 'custom serializer not called') }) test('reply.serialize should serialize payload with a context default serializer', t => { t.plan(2) let customSerializerCalled = false const response = { statusCode: 200 } const context = { [kReplySerializerDefault]: (x) => (customSerializerCalled = true) && JSON.stringify(x) } const reply = new Reply(response, { [kRouteContext]: context }) t.equal(reply.serialize({ foo: 'bar' }), '{"foo":"bar"}') t.equal(customSerializerCalled, true, 'custom serializer not called') }) test('reply.serialize should serialize payload with Fastify instance', t => { t.plan(2) const fastify = Fastify() t.teardown(fastify.close.bind(fastify)) fastify.route({ method: 'GET', url: '/', schema: { response: { 200: { type: 'object', properties: { foo: { type: 'string' } } } } }, handler: (req, reply) => { reply.send( reply.serialize({ foo: 'bar' }) ) } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, '{"foo":"bar"}') }) }) test('within an instance', t => { const fastify = Fastify() t.teardown(fastify.close.bind(fastify)) const test = t.test fastify.get('/', function (req, reply) { reply.code(200) reply.header('Content-Type', 'text/plain') reply.send('hello world!') }) fastify.get('/auto-type', function (req, reply) { reply.code(200) reply.type('text/plain') reply.send('hello world!') }) fastify.get('/auto-status-code', function (req, reply) { reply.send('hello world!') }) fastify.get('/redirect', function (req, reply) { reply.redirect('/') }) fastify.get('/redirect-async', async function (req, reply) { return reply.redirect('/') }) fastify.get('/redirect-code', function (req, reply) { reply.redirect('/', 301) }) fastify.get('/redirect-code-before-call', function (req, reply) { reply.code(307).redirect('/') }) fastify.get('/redirect-code-before-call-overwrite', function (req, reply) { reply.code(307).redirect('/', 302) }) fastify.get('/custom-serializer', function (req, reply) { reply.code(200) reply.type('text/plain') reply.serializer(function (body) { return require('node:querystring').stringify(body) }) reply.send({ hello: 'world!' }) }) fastify.register(function (instance, options, done) { fastify.addHook('onSend', function (req, reply, payload, done) { reply.header('x-onsend', 'yes') done() }) fastify.get('/redirect-onsend', function (req, reply) { reply.redirect('/') }) done() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) test('custom serializer should be used', t => { t.plan(3) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/custom-serializer' }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/plain') t.same(body.toString(), 'hello=world!') }) }) test('status code and content-type should be correct', t => { t.plan(4) 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-type'], 'text/plain') t.same(body.toString(), 'hello world!') }) }) test('auto status code should be 200', t => { t.plan(3) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/auto-status-code' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.same(body.toString(), 'hello world!') }) }) test('auto type should be text/plain', t => { t.plan(3) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/auto-type' }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/plain') t.same(body.toString(), 'hello world!') }) }) test('redirect to `/` - 1', t => { t.plan(1) http.get('http://127.0.0.1:' + fastify.server.address().port + '/redirect', function (response) { t.equal(response.statusCode, 302) }) }) test('redirect to `/` - 2', t => { t.plan(1) http.get('http://127.0.0.1:' + fastify.server.address().port + '/redirect-code', function (response) { t.equal(response.statusCode, 301) }) }) test('redirect to `/` - 3', t => { t.plan(4) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/redirect' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-type'], 'text/plain') t.same(body.toString(), 'hello world!') }) }) test('redirect to `/` - 4', t => { t.plan(4) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/redirect-code' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-type'], 'text/plain') t.same(body.toString(), 'hello world!') }) }) test('redirect to `/` - 5', t => { t.plan(3) const url = 'http://127.0.0.1:' + fastify.server.address().port + '/redirect-onsend' http.get(url, (response) => { t.equal(response.headers['x-onsend'], 'yes') t.equal(response.headers['content-length'], '0') t.equal(response.headers.location, '/') }) }) test('redirect to `/` - 6', t => { t.plan(4) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/redirect-code-before-call' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-type'], 'text/plain') t.same(body.toString(), 'hello world!') }) }) test('redirect to `/` - 7', t => { t.plan(4) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/redirect-code-before-call-overwrite' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 200) t.equal(response.headers['content-type'], 'text/plain') t.same(body.toString(), 'hello world!') }) }) test('redirect to `/` - 8', t => { t.plan(1) http.get('http://127.0.0.1:' + fastify.server.address().port + '/redirect-code-before-call', function (response) { t.equal(response.statusCode, 307) }) }) test('redirect to `/` - 9', t => { t.plan(1) http.get('http://127.0.0.1:' + fastify.server.address().port + '/redirect-code-before-call-overwrite', function (response) { t.equal(response.statusCode, 302) }) }) test('redirect with async function to `/` - 10', t => { t.plan(1) http.get('http://127.0.0.1:' + fastify.server.address().port + '/redirect-async', function (response) { t.equal(response.statusCode, 302) }) }) t.end() }) }) test('buffer without content type should send a application/octet-stream and raw buffer', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.send(Buffer.alloc(1024)) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'application/octet-stream') t.same(body, Buffer.alloc(1024)) }) }) }) test('Uint8Array without content type should send a application/octet-stream and raw buffer', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.send(new Uint8Array(1024).fill(0xff)) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) fastify.inject({ method: 'GET', url: '/' }, (err, response) => { t.error(err) t.equal(response.headers['content-type'], 'application/octet-stream') t.same(new Uint8Array(response.rawPayload), new Uint8Array(1024).fill(0xff)) }) }) }) test('Uint16Array without content type should send a application/octet-stream and raw buffer', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.send(new Uint16Array(50).fill(0xffffffff)) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/octet-stream') t.same(new Uint16Array(res.rawPayload.buffer, res.rawPayload.byteOffset, res.rawPayload.byteLength / Uint16Array.BYTES_PER_ELEMENT), new Uint16Array(50).fill(0xffffffff)) }) }) }) test('TypedArray with content type should not send application/octet-stream', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.header('Content-Type', 'text/plain') reply.send(new Uint16Array(1024).fill(0xffffffff)) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'text/plain') t.same(new Uint16Array(res.rawPayload.buffer, res.rawPayload.byteOffset, res.rawPayload.byteLength / Uint16Array.BYTES_PER_ELEMENT), new Uint16Array(1024).fill(0xffffffff)) }) }) }) test('buffer with content type should not send application/octet-stream', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.header('Content-Type', 'text/plain') reply.send(Buffer.alloc(1024)) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/plain') t.same(body, Buffer.alloc(1024)) }) }) }) test('stream with content type should not send application/octet-stream', t => { t.plan(4) const fastify = Fastify() const streamPath = path.join(__dirname, '..', '..', 'package.json') const stream = fs.createReadStream(streamPath) const buf = fs.readFileSync(streamPath) fastify.get('/', function (req, reply) { reply.header('Content-Type', 'text/plain').send(stream) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/plain') t.same(body, buf) }) }) }) test('stream without content type should not send application/octet-stream', t => { t.plan(4) const fastify = Fastify() const stream = fs.createReadStream(__filename) const buf = fs.readFileSync(__filename) fastify.get('/', function (req, reply) { reply.send(stream) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], undefined) t.same(body, buf) }) }) }) test('stream using reply.raw.writeHead should return customize headers', t => { t.plan(6) const fastify = Fastify() const fs = require('node:fs') const path = require('node:path') const streamPath = path.join(__dirname, '..', '..', 'package.json') const stream = fs.createReadStream(streamPath) const buf = fs.readFileSync(streamPath) fastify.get('/', function (req, reply) { reply.log.warn = function mockWarn (message) { t.equal(message, 'response will send, but you shouldn\'t use res.writeHead in stream mode') } reply.raw.writeHead(200, { location: '/' }) reply.send(stream) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers.location, '/') t.equal(response.headers['Content-Type'], undefined) t.same(body, buf) }) }) }) test('plain string without content type should send a text/plain', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.send('hello world!') }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') t.same(body.toString(), 'hello world!') }) }) }) test('plain string with content type should be sent unmodified', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.type('text/css').send('hello world!') }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/css') t.same(body.toString(), 'hello world!') }) }) }) test('plain string with content type and custom serializer should be serialized', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply .serializer(() => 'serialized') .type('text/css') .send('hello world!') }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'text/css') t.same(body.toString(), 'serialized') }) }) }) test('plain string with content type application/json should NOT be serialized as json', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.type('application/json').send('{"key": "hello world!"}') }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'application/json; charset=utf-8') t.same(body.toString(), '{"key": "hello world!"}') }) }) }) test('plain string with custom json content type should NOT be serialized as json', t => { t.plan(19) const fastify = Fastify() const customSamples = { collectionjson: { mimeType: 'application/vnd.collection+json', sample: '{"collection":{"version":"1.0","href":"http://api.example.com/people/"}}' }, hal: { mimeType: 'application/hal+json', sample: '{"_links":{"self":{"href":"https://api.example.com/people/1"}},"name":"John Doe"}' }, jsonapi: { mimeType: 'application/vnd.api+json', sample: '{"data":{"type":"people","id":"1"}}' }, jsonld: { mimeType: 'application/ld+json', sample: '{"@context":"https://json-ld.org/contexts/person.jsonld","name":"John Doe"}' }, ndjson: { mimeType: 'application/x-ndjson', sample: '{"a":"apple","b":{"bb":"bubble"}}\n{"c":"croissant","bd":{"dd":"dribble"}}' }, siren: { mimeType: 'application/vnd.siren+json', sample: '{"class":"person","properties":{"name":"John Doe"}}' } } Object.keys(customSamples).forEach((path) => { fastify.get(`/${path}`, function (req, reply) { reply.type(customSamples[path].mimeType).send(customSamples[path].sample) }) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) Object.keys(customSamples).forEach((path) => { sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/' + path }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], customSamples[path].mimeType + '; charset=utf-8') t.same(body.toString(), customSamples[path].sample) }) }) }) }) test('non-string with content type application/json SHOULD be serialized as json', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.type('application/json').send({ key: 'hello world!' }) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'application/json; charset=utf-8') t.same(body.toString(), JSON.stringify({ key: 'hello world!' })) }) }) }) test('non-string with custom json\'s content-type SHOULD be serialized as json', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.type('application/json; version=2; ').send({ key: 'hello world!' }) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], 'application/json; version=2; charset=utf-8') t.same(body.toString(), JSON.stringify({ key: 'hello world!' })) }) }) }) test('non-string with custom json content type SHOULD be serialized as json', t => { t.plan(16) const fastify = Fastify() const customSamples = { collectionjson: { mimeType: 'application/vnd.collection+json', sample: JSON.parse('{"collection":{"version":"1.0","href":"http://api.example.com/people/"}}') }, hal: { mimeType: 'application/hal+json', sample: JSON.parse('{"_links":{"self":{"href":"https://api.example.com/people/1"}},"name":"John Doe"}') }, jsonapi: { mimeType: 'application/vnd.api+json', sample: JSON.parse('{"data":{"type":"people","id":"1"}}') }, jsonld: { mimeType: 'application/ld+json', sample: JSON.parse('{"@context":"https://json-ld.org/contexts/person.jsonld","name":"John Doe"}') }, siren: { mimeType: 'application/vnd.siren+json', sample: JSON.parse('{"class":"person","properties":{"name":"John Doe"}}') } } Object.keys(customSamples).forEach((path) => { fastify.get(`/${path}`, function (req, reply) { reply.type(customSamples[path].mimeType).send(customSamples[path].sample) }) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) Object.keys(customSamples).forEach((path) => { sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/' + path }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], customSamples[path].mimeType + '; charset=utf-8') t.same(body.toString(), JSON.stringify(customSamples[path].sample)) }) }) }) }) test('error object with a content type that is not application/json should work', t => { t.plan(6) const fastify = Fastify() fastify.get('/text', function (req, reply) { reply.type('text/plain') reply.send(new Error('some application error')) }) fastify.get('/html', function (req, reply) { reply.type('text/html') reply.send(new Error('some application error')) }) fastify.inject({ method: 'GET', url: '/text' }, (err, res) => { t.error(err) t.equal(res.statusCode, 500) t.equal(JSON.parse(res.payload).message, 'some application error') }) fastify.inject({ method: 'GET', url: '/html' }, (err, res) => { t.error(err) t.equal(res.statusCode, 500) t.equal(JSON.parse(res.payload).message, 'some application error') }) }) test('undefined payload should be sent as-is', t => { t.plan(6) const fastify = Fastify() fastify.addHook('onSend', function (request, reply, payload, done) { t.equal(payload, undefined) done() }) fastify.get('/', function (req, reply) { reply.code(204).send() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: `http://127.0.0.1:${fastify.server.address().port}` }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], undefined) t.equal(response.headers['content-length'], undefined) t.equal(body.length, 0) }) }) }) test('for HEAD method, no body should be sent but content-length should be', t => { t.plan(11) const fastify = Fastify() const contentType = 'application/json; charset=utf-8' const bodySize = JSON.stringify({ foo: 'bar' }).length fastify.head('/', { onSend: function (request, reply, payload, done) { t.equal(payload, undefined) done() } }, function (req, reply) { reply.header('content-length', bodySize) reply.header('content-type', contentType) reply.code(200).send() }) fastify.head('/with/null', { onSend: function (request, reply, payload, done) { t.equal(payload, 'null') done() } }, function (req, reply) { reply.header('content-length', bodySize) reply.header('content-type', contentType) reply.code(200).send(null) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'HEAD', url: `http://127.0.0.1:${fastify.server.address().port}` }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], contentType) t.equal(response.headers['content-length'], bodySize.toString()) t.equal(body.length, 0) }) sget({ method: 'HEAD', url: `http://127.0.0.1:${fastify.server.address().port}/with/null` }, (err, response, body) => { t.error(err) t.equal(response.headers['content-type'], contentType) t.equal(response.headers['content-length'], bodySize.toString()) t.equal(body.length, 0) }) }) }) test('reply.send(new NotFound()) should not invoke the 404 handler', t => { t.plan(9) const fastify = Fastify() fastify.setNotFoundHandler((req, reply) => { t.fail('Should not be called') }) fastify.get('/not-found', function (req, reply) { reply.send(new NotFound()) }) fastify.register(function (instance, options, done) { instance.get('/not-found', function (req, reply) { reply.send(new NotFound()) }) done() }, { prefix: '/prefixed' }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/not-found' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) t.equal(response.headers['content-type'], 'application/json; charset=utf-8') t.same(JSON.parse(body.toString()), { statusCode: 404, error: 'Not Found', message: 'Not Found' }) }) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/prefixed/not-found' }, (err, response, body) => { t.error(err) t.equal(response.statusCode, 404) t.equal(response.headers['content-type'], 'application/json; charset=utf-8') t.same(JSON.parse(body), { error: 'Not Found', message: 'Not Found', statusCode: 404 }) }) }) }) test('reply can set multiple instances of same header', t => { t.plan(4) const fastify = require('../../')() fastify.get('/headers', function (req, reply) { reply .header('set-cookie', 'one') .header('set-cookie', 'two') .send({}) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, (err, response, body) => { t.error(err) t.ok(response.headers['set-cookie']) t.strictSame(response.headers['set-cookie'], ['one', 'two']) }) }) }) test('reply.hasHeader returns correct values', t => { t.plan(3) const fastify = require('../../')() fastify.get('/headers', function (req, reply) { reply.header('x-foo', 'foo') t.equal(reply.hasHeader('x-foo'), true) t.equal(reply.hasHeader('x-bar'), false) reply.send() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, () => {}) }) }) test('reply.getHeader returns correct values', t => { t.plan(5) const fastify = require('../../')() fastify.get('/headers', function (req, reply) { reply.header('x-foo', 'foo') t.equal(reply.getHeader('x-foo'), 'foo') reply.header('x-foo', 'bar') t.strictSame(reply.getHeader('x-foo'), 'bar') reply.header('x-foo', 42) t.strictSame(reply.getHeader('x-foo'), 42) reply.header('set-cookie', 'one') reply.header('set-cookie', 'two') t.strictSame(reply.getHeader('set-cookie'), ['one', 'two']) reply.send() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, () => {}) }) }) test('reply.getHeader returns raw header if there is not in the reply headers', t => { t.plan(1) const response = { setHeader: () => {}, hasHeader: () => true, getHeader: () => 'bar', writeHead: () => {}, end: () => {} } const reply = new Reply(response, { onSend: [] }, null) t.equal(reply.getHeader('foo'), 'bar') }) test('reply.getHeaders returns correct values', t => { t.plan(3) const fastify = require('../../')() fastify.get('/headers', function (req, reply) { reply.header('x-foo', 'foo') t.strictSame(reply.getHeaders(), { 'x-foo': 'foo' }) reply.header('x-bar', 'bar') reply.raw.setHeader('x-foo', 'foo2') reply.raw.setHeader('x-baz', 'baz') t.strictSame(reply.getHeaders(), { 'x-foo': 'foo', 'x-bar': 'bar', 'x-baz': 'baz' }) reply.send() }) fastify.inject('/headers', (err) => { t.error(err) }) }) test('reply.removeHeader can remove the value', t => { t.plan(5) const fastify = require('../../')() t.teardown(fastify.close.bind(fastify)) fastify.get('/headers', function (req, reply) { reply.header('x-foo', 'foo') t.equal(reply.getHeader('x-foo'), 'foo') t.equal(reply.removeHeader('x-foo'), reply) t.strictSame(reply.getHeader('x-foo'), undefined) reply.send() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, () => { t.pass() }) }) }) test('reply.header can reset the value', t => { t.plan(3) const fastify = require('../../')() t.teardown(fastify.close.bind(fastify)) fastify.get('/headers', function (req, reply) { reply.header('x-foo', 'foo') reply.header('x-foo', undefined) t.strictSame(reply.getHeader('x-foo'), '') reply.send() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, () => { t.pass() }) }) }) // https://github.com/fastify/fastify/issues/3030 test('reply.hasHeader computes raw and fastify headers', t => { t.plan(4) const fastify = require('../../')() t.teardown(fastify.close.bind(fastify)) fastify.get('/headers', function (req, reply) { reply.header('x-foo', 'foo') reply.raw.setHeader('x-bar', 'bar') t.ok(reply.hasHeader('x-foo')) t.ok(reply.hasHeader('x-bar')) reply.send() }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, () => { t.pass() }) }) }) test('Reply should handle JSON content type with a charset', t => { t.plan(16) const fastify = require('../../')() fastify.get('/default', function (req, reply) { reply.send({ hello: 'world' }) }) fastify.get('/utf8', function (req, reply) { reply .header('content-type', 'application/json; charset=utf-8') .send({ hello: 'world' }) }) fastify.get('/utf16', function (req, reply) { reply .header('content-type', 'application/json; charset=utf-16') .send({ hello: 'world' }) }) fastify.get('/utf32', function (req, reply) { reply .header('content-type', 'application/json; charset=utf-32') .send({ hello: 'world' }) }) fastify.get('/type-utf8', function (req, reply) { reply .type('application/json; charset=utf-8') .send({ hello: 'world' }) }) fastify.get('/type-utf16', function (req, reply) { reply .type('application/json; charset=utf-16') .send({ hello: 'world' }) }) fastify.get('/type-utf32', function (req, reply) { reply .type('application/json; charset=utf-32') .send({ hello: 'world' }) }) fastify.get('/no-space-type-utf32', function (req, reply) { reply .type('application/json;charset=utf-32') .send({ hello: 'world' }) }) fastify.inject('/default', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-8') }) fastify.inject('/utf8', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-8') }) fastify.inject('/utf16', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-16') }) fastify.inject('/utf32', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-32') }) fastify.inject('/type-utf8', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-8') }) fastify.inject('/type-utf16', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-16') }) fastify.inject('/type-utf32', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-32') }) fastify.inject('/no-space-type-utf32', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json;charset=utf-32') }) }) test('Content type and charset set previously', t => { t.plan(2) const fastify = require('../../')() fastify.addHook('onRequest', function (req, reply, done) { reply.header('content-type', 'application/json; charset=utf-16') done() }) fastify.get('/', function (req, reply) { reply.send({ hello: 'world' }) }) fastify.inject('/', (err, res) => { t.error(err) t.equal(res.headers['content-type'], 'application/json; charset=utf-16') }) }) test('.status() is an alias for .code()', t => { t.plan(2) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.status(418).send() }) fastify.inject('/', (err, res) => { t.error(err) t.equal(res.statusCode, 418) }) }) test('.statusCode is getter and setter', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { t.ok(reply.statusCode, 200, 'default status value') reply.statusCode = 418 t.ok(reply.statusCode, 418) reply.send() }) fastify.inject('/', (err, res) => { t.error(err) t.equal(res.statusCode, 418) }) }) test('reply.header setting multiple cookies as multiple Set-Cookie headers', t => { t.plan(7) const fastify = require('../../')() fastify.get('/headers', function (req, reply) { reply .header('set-cookie', 'one') .header('set-cookie', 'two') .header('set-cookie', 'three') .header('set-cookie', ['four', 'five', 'six']) .send({}) }) fastify.listen({ port: 0 }, err => { t.error(err) t.teardown(fastify.close.bind(fastify)) sget({ method: 'GET', url: 'http://127.0.0.1:' + fastify.server.address().port + '/headers' }, (err, response, body) => { t.error(err) t.ok(response.headers['set-cookie']) t.strictSame(response.headers['set-cookie'], ['one', 'two', 'three', 'four', 'five', 'six']) }) }) fastify.inject('/headers', (error, response) => { t.error(error) t.ok(response.headers['set-cookie']) t.strictSame(response.headers['set-cookie'], ['one', 'two', 'three', 'four', 'five', 'six']) }) }) test('should emit deprecation warning when trying to modify the reply.sent property', t => { t.plan(4) const fastify = Fastify() FSTDEP010.emitted = false process.removeAllListeners('warning') process.on('warning', onWarning) function onWarning (warning) { t.equal(warning.name, 'DeprecationWarning') t.equal(warning.code, FSTDEP010.code) } fastify.get('/', (req, reply) => { reply.sent = true reply.raw.end() }) fastify.inject('/', (err, res) => { t.error(err) t.pass() process.removeListener('warning', onWarning) }) }) test('should emit deprecation warning when trying to use the reply.context.config property', t => { t.plan(4) const fastify = Fastify() FSTDEP019.emitted = false process.removeAllListeners('warning') process.on('warning', onWarning) function onWarning (warning) { t.equal(warning.name, 'DeprecationWarning') t.equal(warning.code, FSTDEP019.code) } fastify.get('/', (req, reply) => { req.log(reply.context.config) }) fastify.inject('/', (err, res) => { t.error(err) t.pass() process.removeListener('warning', onWarning) }) }) test('should throw error when passing falsy value to reply.sent', t => { t.plan(4) const fastify = Fastify() fastify.get('/', function (req, reply) { try { reply.sent = false } catch (err) { t.equal(err.code, 'FST_ERR_REP_SENT_VALUE') t.equal(err.message, 'The only possible value for reply.sent is true.') reply.send() } }) fastify.inject('/', (err, res) => { t.error(err) t.pass() }) }) test('should throw error when attempting to set reply.sent more than once', t => { t.plan(3) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.sent = true try { reply.sent = true t.fail('must throw') } catch (err) { t.equal(err.code, 'FST_ERR_REP_ALREADY_SENT') } reply.raw.end() }) fastify.inject('/', (err, res) => { t.error(err) t.pass() }) }) test('should not throw error when attempting to set reply.sent if the underlining request was sent', t => { t.plan(3) const fastify = Fastify() fastify.get('/', function (req, reply) { reply.raw.end() t.doesNotThrow(() => { reply.sent = true }) }) fastify.inject('/', (err, res) => { t.error(err) t.pass() }) }) test('reply.getResponseTime() should return 0 before the timer is initialised on the reply by setting up response listeners', t => { t.plan(1) const response = { statusCode: 200 } const reply = new Reply(response, null) t.equal(reply.getResponseTime(), 0) }) test('reply.getResponseTime() should return a number greater than 0 after the timer is initialised on the reply by setting up response listeners', t => { t.plan(1) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send('hello world') } }) fastify.addHook('onResponse', (req, reply) => { t.ok(reply.getResponseTime() > 0) t.end() }) fastify.inject({ method: 'GET', url: '/' }) }) test('should emit deprecation warning when trying to use reply.getResponseTime() and should return the time since a request started while inflight', t => { t.plan(5) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send('hello world') } }) process.removeAllListeners('warning') process.on('warning', onWarning) function onWarning (warning) { t.equal(warning.name, 'DeprecationWarning') t.equal(warning.code, FSTDEP020.code) } fastify.addHook('preValidation', (req, reply, done) => { t.equal(reply.getResponseTime(), reply.getResponseTime()) done() }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.pass() process.removeListener('warning', onWarning) }) FSTDEP020.emitted = false }) test('reply.getResponseTime() should return the same value after a request is finished', t => { t.plan(1) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send('hello world') } }) fastify.addHook('onResponse', (req, reply) => { t.equal(reply.getResponseTime(), reply.getResponseTime()) t.end() }) fastify.inject({ method: 'GET', url: '/' }) }) test('reply.elapsedTime should return a number greater than 0 after the timer is initialised on the reply by setting up response listeners', t => { t.plan(1) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send('hello world') } }) fastify.addHook('onResponse', (req, reply) => { t.ok(reply.elapsedTime > 0) t.end() }) fastify.inject({ method: 'GET', url: '/' }) }) test('reply.elapsedTime should return the time since a request started while inflight', t => { t.plan(1) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send('hello world') } }) let preValidationElapsedTime fastify.addHook('preValidation', (req, reply, done) => { preValidationElapsedTime = reply.elapsedTime done() }) fastify.addHook('onResponse', (req, reply) => { t.ok(reply.elapsedTime > preValidationElapsedTime) t.end() }) fastify.inject({ method: 'GET', url: '/' }) }) test('reply.elapsedTime should return the same value after a request is finished', t => { t.plan(1) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send('hello world') } }) fastify.addHook('onResponse', (req, reply) => { t.equal(reply.elapsedTime, reply.elapsedTime) t.end() }) fastify.inject({ method: 'GET', url: '/' }) }) test('reply should use the custom serializer', t => { t.plan(4) const fastify = Fastify() fastify.setReplySerializer((payload, statusCode) => { t.same(payload, { foo: 'bar' }) t.equal(statusCode, 200) payload.foo = 'bar bar' return JSON.stringify(payload) }) fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send({ foo: 'bar' }) } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, '{"foo":"bar bar"}') }) }) test('reply should use the right serializer in encapsulated context', t => { t.plan(9) const fastify = Fastify() fastify.setReplySerializer((payload) => { t.same(payload, { foo: 'bar' }) payload.foo = 'bar bar' return JSON.stringify(payload) }) fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send({ foo: 'bar' }) } }) fastify.register(function (instance, opts, done) { instance.route({ method: 'GET', url: '/sub', handler: (req, reply) => { reply.send({ john: 'doo' }) } }) instance.setReplySerializer((payload) => { t.same(payload, { john: 'doo' }) payload.john = 'too too' return JSON.stringify(payload) }) done() }) fastify.register(function (instance, opts, done) { instance.route({ method: 'GET', url: '/sub', handler: (req, reply) => { reply.send({ sweet: 'potato' }) } }) instance.setReplySerializer((payload) => { t.same(payload, { sweet: 'potato' }) payload.sweet = 'potato potato' return JSON.stringify(payload) }) done() }, { prefix: 'sub' }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, '{"foo":"bar bar"}') }) fastify.inject({ method: 'GET', url: '/sub' }, (err, res) => { t.error(err) t.equal(res.payload, '{"john":"too too"}') }) fastify.inject({ method: 'GET', url: '/sub/sub' }, (err, res) => { t.error(err) t.equal(res.payload, '{"sweet":"potato potato"}') }) }) test('reply should use the right serializer in deep encapsulated context', t => { t.plan(8) const fastify = Fastify() fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply.send({ foo: 'bar' }) } }) fastify.register(function (instance, opts, done) { instance.route({ method: 'GET', url: '/sub', handler: (req, reply) => { reply.send({ john: 'doo' }) } }) instance.setReplySerializer((payload) => { t.same(payload, { john: 'doo' }) payload.john = 'too too' return JSON.stringify(payload) }) instance.register(function (subInstance, opts, done) { subInstance.route({ method: 'GET', url: '/deep', handler: (req, reply) => { reply.send({ john: 'deep' }) } }) subInstance.setReplySerializer((payload) => { t.same(payload, { john: 'deep' }) payload.john = 'deep deep' return JSON.stringify(payload) }) done() }) done() }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, '{"foo":"bar"}') }) fastify.inject({ method: 'GET', url: '/sub' }, (err, res) => { t.error(err) t.equal(res.payload, '{"john":"too too"}') }) fastify.inject({ method: 'GET', url: '/deep' }, (err, res) => { t.error(err) t.equal(res.payload, '{"john":"deep deep"}') }) }) test('reply should use the route serializer', t => { t.plan(3) const fastify = Fastify() fastify.setReplySerializer(() => { t.fail('this serializer should not be executed') }) fastify.route({ method: 'GET', url: '/', handler: (req, reply) => { reply .serializer((payload) => { t.same(payload, { john: 'doo' }) payload.john = 'too too' return JSON.stringify(payload) }) .send({ john: 'doo' }) } }) fastify.inject({ method: 'GET', url: '/' }, (err, res) => { t.error(err) t.equal(res.payload, '{"john":"too too"}') }) }) test('cannot set the replySerializer when the server is running', t => { t.plan(2) const fastify = Fastify() t.teardown(fastify.close.bind(fastify)) fastify.listen({ port: 0 }, err => { t.error(err) try { fastify.setReplySerializer(() => {}) t.fail('this serializer should not b