strong-tie/inbound-calls
0
1'use strict'2 3const { connect } = require('node:net')4const sget = require('simple-get').concat5const t = require('tap')6const test = t.test7const Fastify = require('..')8const { kRequest } = require('../lib/symbols.js')9 10test('default 400 on request error', t => {11 t.plan(4)12 13 const fastify = Fastify()14 15 fastify.post('/', function (req, reply) {16 reply.send({ hello: 'world' })17 })18 19 fastify.inject({20 method: 'POST',21 url: '/',22 simulate: {23 error: true24 },25 body: {26 text: '12345678901234567890123456789012345678901234567890'27 }28 }, (err, res) => {29 t.error(err)30 t.equal(res.statusCode, 400)31 t.equal(res.headers['content-type'], 'application/json; charset=utf-8')32 t.same(JSON.parse(res.payload), {33 error: 'Bad Request',34 message: 'Simulated',35 statusCode: 40036 })37 })38})39 40test('default 400 on request error with custom error handler', t => {41 t.plan(6)42 43 const fastify = Fastify()44 45 fastify.setErrorHandler(function (err, request, reply) {46 t.type(request, 'object')47 t.type(request, fastify[kRequest].parent)48 reply49 .code(err.statusCode)50 .type('application/json; charset=utf-8')51 .send(err)52 })53 54 fastify.post('/', function (req, reply) {55 reply.send({ hello: 'world' })56 })57 58 fastify.inject({59 method: 'POST',60 url: '/',61 simulate: {62 error: true63 },64 body: {65 text: '12345678901234567890123456789012345678901234567890'66 }67 }, (err, res) => {68 t.error(err)69 t.equal(res.statusCode, 400)70 t.equal(res.headers['content-type'], 'application/json; charset=utf-8')71 t.same(JSON.parse(res.payload), {72 error: 'Bad Request',73 message: 'Simulated',74 statusCode: 40075 })76 })77})78 79test('default clientError handler ignores ECONNRESET', t => {80 t.plan(3)81 82 let logs = ''83 let response = ''84 85 const fastify = Fastify({86 bodyLimit: 1,87 keepAliveTimeout: 100,88 logger: {89 level: 'trace',90 stream: {91 write () {92 logs += JSON.stringify(arguments)93 }94 }95 }96 })97 98 fastify.get('/', (request, reply) => {99 reply.send('OK')100 101 process.nextTick(() => {102 const error = new Error()103 error.code = 'ECONNRESET'104 105 fastify.server.emit('clientError', error, request.raw.socket)106 })107 })108 109 fastify.listen({ port: 0 }, function (err) {110 t.error(err)111 t.teardown(() => { fastify.close() })112 113 const client = connect(fastify.server.address().port)114 115 client.on('data', chunk => {116 response += chunk.toString('utf-8')117 })118 119 client.on('end', () => {120 t.match(response, /^HTTP\/1.1 200 OK/)121 t.notMatch(logs, /ECONNRESET/)122 })123 124 client.resume()125 client.write('GET / HTTP/1.1\r\n')126 client.write('Host: example.com\r\n')127 client.write('Connection: close\r\n')128 client.write('\r\n\r\n')129 })130})131 132test('default clientError handler ignores sockets in destroyed state', t => {133 t.plan(1)134 135 const fastify = Fastify({136 bodyLimit: 1,137 keepAliveTimeout: 100138 })139 fastify.server.on('clientError', () => {140 // this handler is called after default handler, so we can make sure end was not called141 t.pass()142 })143 fastify.server.emit('clientError', new Error(), {144 destroyed: true,145 end () {146 t.fail('end should not be called')147 },148 destroy () {149 t.fail('destroy should not be called')150 }151 })152})153 154test('default clientError handler destroys sockets in writable state', t => {155 t.plan(2)156 157 const fastify = Fastify({158 bodyLimit: 1,159 keepAliveTimeout: 100160 })161 162 fastify.server.emit('clientError', new Error(), {163 destroyed: false,164 writable: true,165 encrypted: true,166 end () {167 t.fail('end should not be called')168 },169 destroy () {170 t.pass('destroy should be called')171 },172 write (response) {173 t.match(response, /^HTTP\/1.1 400 Bad Request/)174 }175 })176})177 178test('default clientError handler destroys http sockets in non-writable state', t => {179 t.plan(1)180 181 const fastify = Fastify({182 bodyLimit: 1,183 keepAliveTimeout: 100184 })185 186 fastify.server.emit('clientError', new Error(), {187 destroyed: false,188 writable: false,189 end () {190 t.fail('end should not be called')191 },192 destroy () {193 t.pass('destroy should be called')194 },195 write (response) {196 t.fail('write should not be called')197 }198 })199})200 201test('error handler binding', t => {202 t.plan(5)203 204 const fastify = Fastify()205 206 fastify.setErrorHandler(function (err, request, reply) {207 t.equal(this, fastify)208 reply209 .code(err.statusCode)210 .type('application/json; charset=utf-8')211 .send(err)212 })213 214 fastify.post('/', function (req, reply) {215 reply.send({ hello: 'world' })216 })217 218 fastify.inject({219 method: 'POST',220 url: '/',221 simulate: {222 error: true223 },224 body: {225 text: '12345678901234567890123456789012345678901234567890'226 }227 }, (err, res) => {228 t.error(err)229 t.equal(res.statusCode, 400)230 t.equal(res.headers['content-type'], 'application/json; charset=utf-8')231 t.same(JSON.parse(res.payload), {232 error: 'Bad Request',233 message: 'Simulated',234 statusCode: 400235 })236 })237})238 239test('encapsulated error handler binding', t => {240 t.plan(7)241 242 const fastify = Fastify()243 244 fastify.register(function (app, opts, done) {245 app.decorate('hello', 'world')246 t.equal(app.hello, 'world')247 app.post('/', function (req, reply) {248 reply.send({ hello: 'world' })249 })250 app.setErrorHandler(function (err, request, reply) {251 t.equal(this.hello, 'world')252 reply253 .code(err.statusCode)254 .type('application/json; charset=utf-8')255 .send(err)256 })257 done()258 })259 260 fastify.inject({261 method: 'POST',262 url: '/',263 simulate: {264 error: true265 },266 body: {267 text: '12345678901234567890123456789012345678901234567890'268 }269 }, (err, res) => {270 t.error(err)271 t.equal(res.statusCode, 400)272 t.equal(res.headers['content-type'], 'application/json; charset=utf-8')273 t.same(res.json(), {274 error: 'Bad Request',275 message: 'Simulated',276 statusCode: 400277 })278 t.equal(fastify.hello, undefined)279 })280})281 282test('default clientError replies with bad request on reused keep-alive connection', t => {283 t.plan(2)284 285 let response = ''286 287 const fastify = Fastify({288 bodyLimit: 1,289 keepAliveTimeout: 100290 })291 292 fastify.get('/', (request, reply) => {293 reply.send('OK\n')294 })295 296 fastify.listen({ port: 0 }, function (err) {297 t.error(err)298 fastify.server.unref()299 300 const client = connect(fastify.server.address().port)301 302 client.on('data', chunk => {303 response += chunk.toString('utf-8')304 })305 306 client.on('end', () => {307 t.match(response, /^HTTP\/1.1 200 OK.*HTTP\/1.1 400 Bad Request/s)308 })309 310 client.resume()311 client.write('GET / HTTP/1.1\r\n')312 client.write('Host: example.com\r\n')313 client.write('\r\n\r\n')314 client.write('GET /?a b HTTP/1.1\r\n')315 client.write('Host: example.com\r\n')316 client.write('Connection: close\r\n')317 client.write('\r\n\r\n')318 })319})320 321test('request.routeOptions should be immutable', t => {322 t.plan(14)323 const fastify = Fastify()324 const handler = function (req, res) {325 t.equal('POST', req.routeOptions.method)326 t.equal('/', req.routeOptions.url)327 t.throws(() => { req.routeOptions = null }, new TypeError('Cannot set property routeOptions of #<Request> which has only a getter'))328 t.throws(() => { req.routeOptions.method = 'INVALID' }, new TypeError('Cannot assign to read only property \'method\' of object \'#<Object>\''))329 t.throws(() => { req.routeOptions.url = '//' }, new TypeError('Cannot assign to read only property \'url\' of object \'#<Object>\''))330 t.throws(() => { req.routeOptions.bodyLimit = 0xDEADBEEF }, new TypeError('Cannot assign to read only property \'bodyLimit\' of object \'#<Object>\''))331 t.throws(() => { req.routeOptions.attachValidation = true }, new TypeError('Cannot assign to read only property \'attachValidation\' of object \'#<Object>\''))332 t.throws(() => { req.routeOptions.logLevel = 'invalid' }, new TypeError('Cannot assign to read only property \'logLevel\' of object \'#<Object>\''))333 t.throws(() => { req.routeOptions.version = '95.0.1' }, new TypeError('Cannot assign to read only property \'version\' of object \'#<Object>\''))334 t.throws(() => { req.routeOptions.prefixTrailingSlash = true }, new TypeError('Cannot assign to read only property \'prefixTrailingSlash\' of object \'#<Object>\''))335 t.throws(() => { req.routeOptions.newAttribute = {} }, new TypeError('Cannot add property newAttribute, object is not extensible'))336 337 for (const key of Object.keys(req.routeOptions)) {338 if (typeof req.routeOptions[key] === 'object' && req.routeOptions[key] !== null) {339 t.fail('Object.freeze must run recursively on nested structures to ensure that routeOptions is immutable.')340 }341 }342 343 res.send({})344 }345 fastify.post('/', {346 bodyLimit: 1000,347 handler348 })349 fastify.listen({ port: 0 }, function (err) {350 t.error(err)351 t.teardown(() => { fastify.close() })352 353 sget({354 method: 'POST',355 url: 'http://localhost:' + fastify.server.address().port,356 headers: { 'Content-Type': 'application/json' },357 body: [],358 json: true359 }, (err, response, body) => {360 t.error(err)361 t.equal(response.statusCode, 200)362 })363 })364})365 366test('request.routeOptions.method is an uppercase string /1', t => {367 t.plan(4)368 const fastify = Fastify()369 const handler = function (req, res) {370 t.equal('POST', req.routeOptions.method)371 res.send({})372 }373 374 fastify.post('/', {375 bodyLimit: 1000,376 handler377 })378 fastify.listen({ port: 0 }, function (err) {379 t.error(err)380 t.teardown(() => { fastify.close() })381 382 sget({383 method: 'POST',384 url: 'http://localhost:' + fastify.server.address().port,385 headers: { 'Content-Type': 'application/json' },386 body: [],387 json: true388 }, (err, response, body) => {389 t.error(err)390 t.equal(response.statusCode, 200)391 })392 })393})394 395test('request.routeOptions.method is an uppercase string /2', t => {396 t.plan(4)397 const fastify = Fastify()398 const handler = function (req, res) {399 t.equal('POST', req.routeOptions.method)400 res.send({})401 }402 403 fastify.route({404 url: '/',405 method: 'POST',406 bodyLimit: 1000,407 handler408 })409 fastify.listen({ port: 0 }, function (err) {410 t.error(err)411 t.teardown(() => { fastify.close() })412 413 sget({414 method: 'POST',415 url: 'http://localhost:' + fastify.server.address().port,416 headers: { 'Content-Type': 'application/json' },417 body: [],418 json: true419 }, (err, response, body) => {420 t.error(err)421 t.equal(response.statusCode, 200)422 })423 })424})425 426test('request.routeOptions.method is an uppercase string /3', t => {427 t.plan(4)428 const fastify = Fastify()429 const handler = function (req, res) {430 t.equal('POST', req.routeOptions.method)431 res.send({})432 }433 434 fastify.route({435 url: '/',436 method: 'pOSt',437 bodyLimit: 1000,438 handler439 })440 fastify.listen({ port: 0 }, function (err) {441 t.error(err)442 t.teardown(() => { fastify.close() })443 444 sget({445 method: 'POST',446 url: 'http://localhost:' + fastify.server.address().port,447 headers: { 'Content-Type': 'application/json' },448 body: [],449 json: true450 }, (err, response, body) => {451 t.error(err)452 t.equal(response.statusCode, 200)453 })454 })455})456 457test('request.routeOptions.method is an array with uppercase string', t => {458 t.plan(4)459 const fastify = Fastify()460 const handler = function (req, res) {461 t.strictSame(['POST'], req.routeOptions.method)462 res.send({})463 }464 465 fastify.route({466 url: '/',467 method: ['pOSt'],468 bodyLimit: 1000,469 handler470 })471 fastify.listen({ port: 0 }, function (err) {472 t.error(err)473 t.teardown(() => { fastify.close() })474 475 sget({476 method: 'POST',477 url: 'http://localhost:' + fastify.server.address().port,478 headers: { 'Content-Type': 'application/json' },479 body: [],480 json: true481 }, (err, response, body) => {482 t.error(err)483 t.equal(response.statusCode, 200)484 })485 })486})487 488test('test request.routeOptions.version', t => {489 t.plan(7)490 const fastify = Fastify()491 492 fastify.route({493 method: 'POST',494 url: '/version',495 constraints: { version: '1.2.0' },496 handler: function (request, reply) {497 t.equal('1.2.0', request.routeOptions.version)498 reply.send({})499 }500 })501 502 fastify.route({503 method: 'POST',504 url: '/version-undefined',505 handler: function (request, reply) {506 t.equal(undefined, request.routeOptions.version)507 reply.send({})508 }509 })510 fastify.listen({ port: 0 }, function (err) {511 t.error(err)512 t.teardown(() => { fastify.close() })513 514 sget({515 method: 'POST',516 url: 'http://localhost:' + fastify.server.address().port + '/version',517 headers: { 'Content-Type': 'application/json', 'Accept-Version': '1.2.0' },518 body: [],519 json: true520 }, (err, response, body) => {521 t.error(err)522 t.equal(response.statusCode, 200)523 })524 525 sget({526 method: 'POST',527 url: 'http://localhost:' + fastify.server.address().port + '/version-undefined',528 headers: { 'Content-Type': 'application/json' },529 body: [],530 json: true531 }, (err, response, body) => {532 t.error(err)533 t.equal(response.statusCode, 200)534 })535 })536})537 