strong-tie/inbound-calls
0
1'use strict'2 3const t = require('tap')4const test = t.test5const net = require('node:net')6const Fastify = require('..')7const statusCodes = require('node:http').STATUS_CODES8const split = require('split2')9const fs = require('node:fs')10const path = require('node:path')11 12const codes = Object.keys(statusCodes)13codes.forEach(code => {14 if (Number(code) >= 400) helper(code)15})16 17function helper (code) {18 test('Reply error handling - code: ' + code, t => {19 t.plan(4)20 const fastify = Fastify()21 t.teardown(fastify.close.bind(fastify))22 const err = new Error('winter is coming')23 24 fastify.get('/', (req, reply) => {25 reply26 .code(Number(code))27 .send(err)28 })29 30 fastify.inject({31 method: 'GET',32 url: '/'33 }, (error, res) => {34 t.error(error)35 t.equal(res.statusCode, Number(code))36 t.equal(res.headers['content-type'], 'application/json; charset=utf-8')37 t.same(38 {39 error: statusCodes[code],40 message: err.message,41 statusCode: Number(code)42 },43 JSON.parse(res.payload)44 )45 })46 })47}48 49test('preHandler hook error handling with external code', t => {50 t.plan(3)51 const fastify = Fastify()52 t.teardown(fastify.close.bind(fastify))53 const err = new Error('winter is coming')54 55 fastify.addHook('preHandler', (req, reply, done) => {56 reply.code(400)57 done(err)58 })59 60 fastify.get('/', () => {})61 62 fastify.inject({63 method: 'GET',64 url: '/'65 }, (error, res) => {66 t.error(error)67 t.equal(res.statusCode, 400)68 t.same(69 {70 error: statusCodes['400'],71 message: err.message,72 statusCode: 40073 },74 JSON.parse(res.payload)75 )76 })77})78 79test('onRequest hook error handling with external done', t => {80 t.plan(3)81 const fastify = Fastify()82 t.teardown(fastify.close.bind(fastify))83 const err = new Error('winter is coming')84 85 fastify.addHook('onRequest', (req, reply, done) => {86 reply.code(400)87 done(err)88 })89 90 fastify.get('/', () => {})91 92 fastify.inject({93 method: 'GET',94 url: '/'95 }, (error, res) => {96 t.error(error)97 t.equal(res.statusCode, 400)98 t.same(99 {100 error: statusCodes['400'],101 message: err.message,102 statusCode: 400103 },104 JSON.parse(res.payload)105 )106 })107})108 109test('Should reply 400 on client error', t => {110 t.plan(2)111 112 const fastify = Fastify()113 t.teardown(fastify.close.bind(fastify))114 fastify.listen({ port: 0, host: '127.0.0.1' }, err => {115 t.error(err)116 117 const client = net.connect(fastify.server.address().port, '127.0.0.1')118 client.end('oooops!')119 120 let chunks = ''121 client.on('data', chunk => {122 chunks += chunk123 })124 125 client.once('end', () => {126 const body = JSON.stringify({127 error: 'Bad Request',128 message: 'Client Error',129 statusCode: 400130 })131 t.equal(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`, chunks)132 })133 })134})135 136test('Should set the response from client error handler', t => {137 t.plan(5)138 139 const responseBody = JSON.stringify({140 error: 'Ended Request',141 message: 'Serious Client Error',142 statusCode: 400143 })144 const response = `HTTP/1.1 400 Bad Request\r\nContent-Length: ${responseBody.length}\r\nContent-Type: application/json; charset=utf-8\r\n\r\n${responseBody}`145 146 function clientErrorHandler (err, socket) {147 t.type(err, Error)148 149 this.log.warn({ err }, 'Handled client error')150 socket.end(response)151 }152 153 const logStream = split(JSON.parse)154 const fastify = Fastify({155 clientErrorHandler,156 logger: {157 stream: logStream,158 level: 'warn'159 }160 })161 162 fastify.listen({ port: 0, host: '127.0.0.1' }, err => {163 t.error(err)164 t.teardown(fastify.close.bind(fastify))165 166 const client = net.connect(fastify.server.address().port, '127.0.0.1')167 client.end('oooops!')168 169 let chunks = ''170 client.on('data', chunk => {171 chunks += chunk172 })173 174 client.once('end', () => {175 t.equal(response, chunks)176 })177 })178 179 logStream.once('data', line => {180 t.equal('Handled client error', line.msg)181 t.equal(40, line.level, 'Log level is not warn')182 })183})184 185test('Error instance sets HTTP status code', t => {186 t.plan(3)187 const fastify = Fastify()188 t.teardown(fastify.close.bind(fastify))189 const err = new Error('winter is coming')190 err.statusCode = 418191 192 fastify.get('/', () => {193 return Promise.reject(err)194 })195 196 fastify.inject({197 method: 'GET',198 url: '/'199 }, (error, res) => {200 t.error(error)201 t.equal(res.statusCode, 418)202 t.same(203 {204 error: statusCodes['418'],205 message: err.message,206 statusCode: 418207 },208 JSON.parse(res.payload)209 )210 })211})212 213test('Error status code below 400 defaults to 500', t => {214 t.plan(3)215 const fastify = Fastify()216 t.teardown(fastify.close.bind(fastify))217 const err = new Error('winter is coming')218 err.statusCode = 399219 220 fastify.get('/', () => {221 return Promise.reject(err)222 })223 224 fastify.inject({225 method: 'GET',226 url: '/'227 }, (error, res) => {228 t.error(error)229 t.equal(res.statusCode, 500)230 t.same(231 {232 error: statusCodes['500'],233 message: err.message,234 statusCode: 500235 },236 JSON.parse(res.payload)237 )238 })239})240 241test('Error.status property support', t => {242 t.plan(3)243 const fastify = Fastify()244 t.teardown(fastify.close.bind(fastify))245 const err = new Error('winter is coming')246 err.status = 418247 248 fastify.get('/', () => {249 return Promise.reject(err)250 })251 252 fastify.inject({253 method: 'GET',254 url: '/'255 }, (error, res) => {256 t.error(error)257 t.equal(res.statusCode, 418)258 t.same(259 {260 error: statusCodes['418'],261 message: err.message,262 statusCode: 418263 },264 JSON.parse(res.payload)265 )266 })267})268 269test('Support rejection with values that are not Error instances', t => {270 const objs = [271 0,272 '',273 [],274 {},275 null,276 undefined,277 123,278 'abc',279 new RegExp(),280 new Date(),281 new Uint8Array()282 ]283 t.plan(objs.length)284 for (const nonErr of objs) {285 t.test('Type: ' + typeof nonErr, t => {286 t.plan(4)287 const fastify = Fastify()288 t.teardown(fastify.close.bind(fastify))289 290 fastify.get('/', () => {291 return Promise.reject(nonErr)292 })293 294 fastify.setErrorHandler((err, request, reply) => {295 if (typeof err === 'object') {296 t.same(err, nonErr)297 } else {298 t.equal(err, nonErr)299 }300 reply.code(500).send('error')301 })302 303 fastify.inject({304 method: 'GET',305 url: '/'306 }, (error, res) => {307 t.error(error)308 t.equal(res.statusCode, 500)309 t.equal(res.payload, 'error')310 })311 })312 }313})314 315test('invalid schema - ajv', t => {316 t.plan(4)317 318 const fastify = Fastify()319 t.teardown(fastify.close.bind(fastify))320 fastify.get('/', {321 schema: {322 querystring: {323 type: 'object',324 properties: {325 id: { type: 'number' }326 }327 }328 }329 }, (req, reply) => {330 t.fail('we should not be here')331 })332 333 fastify.setErrorHandler((err, request, reply) => {334 t.ok(Array.isArray(err.validation))335 reply.code(400).send('error')336 })337 338 fastify.inject({339 url: '/?id=abc',340 method: 'GET'341 }, (err, res) => {342 t.error(err)343 t.equal(res.statusCode, 400)344 t.equal(res.payload, 'error')345 })346})347 348test('should set the status code and the headers from the error object (from route handler) (no custom error handler)', t => {349 t.plan(4)350 const fastify = Fastify()351 t.teardown(fastify.close.bind(fastify))352 353 fastify.get('/', (req, reply) => {354 const error = new Error('kaboom')355 error.headers = { hello: 'world' }356 error.statusCode = 400357 reply.send(error)358 })359 360 fastify.inject({361 url: '/',362 method: 'GET'363 }, (err, res) => {364 t.error(err)365 t.equal(res.statusCode, 400)366 t.equal(res.headers.hello, 'world')367 t.same(JSON.parse(res.payload), {368 error: 'Bad Request',369 message: 'kaboom',370 statusCode: 400371 })372 })373})374 375test('should set the status code and the headers from the error object (from custom error handler)', t => {376 t.plan(6)377 const fastify = Fastify()378 t.teardown(fastify.close.bind(fastify))379 380 fastify.get('/', (req, reply) => {381 const error = new Error('ouch')382 error.statusCode = 401383 reply.send(error)384 })385 386 fastify.setErrorHandler((err, request, reply) => {387 t.equal(err.message, 'ouch')388 t.equal(reply.raw.statusCode, 200)389 const error = new Error('kaboom')390 error.headers = { hello: 'world' }391 error.statusCode = 400392 reply.send(error)393 })394 395 fastify.inject({396 url: '/',397 method: 'GET'398 }, (err, res) => {399 t.error(err)400 t.equal(res.statusCode, 400)401 t.equal(res.headers.hello, 'world')402 t.same(JSON.parse(res.payload), {403 error: 'Bad Request',404 message: 'kaboom',405 statusCode: 400406 })407 })408})409 410// Issue 595 https://github.com/fastify/fastify/issues/595411test('\'*\' should throw an error due to serializer can not handle the payload type', t => {412 t.plan(3)413 const fastify = Fastify()414 t.teardown(fastify.close.bind(fastify))415 416 fastify.get('/', (req, reply) => {417 reply.type('text/html')418 try {419 reply.send({})420 } catch (err) {421 t.type(err, TypeError)422 t.equal(err.code, 'FST_ERR_REP_INVALID_PAYLOAD_TYPE')423 t.equal(err.message, "Attempted to send payload of invalid type 'object'. Expected a string or Buffer.")424 }425 })426 427 fastify.inject({428 url: '/',429 method: 'GET'430 }, (e, res) => {431 t.fail('should not be called')432 })433})434 435test('should throw an error if the custom serializer does not serialize the payload to a valid type', t => {436 t.plan(3)437 const fastify = Fastify()438 t.teardown(fastify.close.bind(fastify))439 440 fastify.get('/', (req, reply) => {441 try {442 reply443 .type('text/html')444 .serializer(payload => payload)445 .send({})446 } catch (err) {447 t.type(err, TypeError)448 t.equal(err.code, 'FST_ERR_REP_INVALID_PAYLOAD_TYPE')449 t.equal(err.message, "Attempted to send payload of invalid type 'object'. Expected a string or Buffer.")450 }451 })452 453 fastify.inject({454 url: '/',455 method: 'GET'456 }, (e, res) => {457 t.fail('should not be called')458 })459})460 461test('should not set headers or status code for custom error handler', t => {462 t.plan(7)463 464 const fastify = Fastify()465 t.teardown(fastify.close.bind(fastify))466 fastify.get('/', function (req, reply) {467 const err = new Error('kaboom')468 err.headers = {469 'fake-random-header': 'abc'470 }471 reply.send(err)472 })473 474 fastify.setErrorHandler(async (err, req, res) => {475 t.equal(res.statusCode, 200)476 t.equal('fake-random-header' in res.headers, false)477 return res.code(500).send(err.message)478 })479 480 fastify.inject({481 method: 'GET',482 url: '/'483 }, (err, res) => {484 t.error(err)485 t.equal(res.statusCode, 500)486 t.equal('fake-random-header' in res.headers, false)487 t.equal(res.headers['content-length'], ('kaboom'.length).toString())488 t.same(res.payload, 'kaboom')489 })490})491 492test('error thrown by custom error handler routes to default error handler', t => {493 t.plan(6)494 495 const fastify = Fastify()496 t.teardown(fastify.close.bind(fastify))497 498 const error = new Error('kaboom')499 error.headers = {500 'fake-random-header': 'abc'501 }502 503 fastify.get('/', function (req, reply) {504 reply.send(error)505 })506 507 const newError = new Error('kabong')508 509 fastify.setErrorHandler(async (err, req, res) => {510 t.equal(res.statusCode, 200)511 t.equal('fake-random-header' in res.headers, false)512 t.same(err.headers, error.headers)513 514 return res.send(newError)515 })516 517 fastify.inject({518 method: 'GET',519 url: '/'520 }, (err, res) => {521 t.error(err)522 t.equal(res.statusCode, 500)523 t.same(JSON.parse(res.payload), {524 error: statusCodes['500'],525 message: newError.message,526 statusCode: 500527 })528 })529})530 531// Refs: https://github.com/fastify/fastify/pull/4484#issuecomment-1367301750532test('allow re-thrown error to default error handler when route handler is async and error handler is sync', t => {533 t.plan(4)534 const fastify = Fastify()535 t.teardown(fastify.close.bind(fastify))536 537 fastify.setErrorHandler(function (error) {538 t.equal(error.message, 'kaboom')539 throw Error('kabong')540 })541 542 fastify.get('/', async function () {543 throw Error('kaboom')544 })545 546 fastify.inject({547 url: '/',548 method: 'GET'549 }, (err, res) => {550 t.error(err)551 t.equal(res.statusCode, 500)552 t.same(JSON.parse(res.payload), {553 error: statusCodes['500'],554 message: 'kabong',555 statusCode: 500556 })557 })558})559 560// Issue 2078 https://github.com/fastify/fastify/issues/2078561// Supported error code list: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml562const invalidErrorCodes = [563 undefined,564 null,565 'error_code',566 567 // out of the 100-599 range:568 0,569 1,570 99,571 600,572 700573]574invalidErrorCodes.forEach((invalidCode) => {575 test(`should throw error if error code is ${invalidCode}`, t => {576 t.plan(2)577 const fastify = Fastify()578 t.teardown(fastify.close.bind(fastify))579 fastify.get('/', (request, reply) => {580 try {581 return reply.code(invalidCode).send('You should not read this')582 } catch (err) {583 t.equal(err.code, 'FST_ERR_BAD_STATUS_CODE')584 t.equal(err.message, 'Called reply with an invalid status code: ' + invalidCode)585 }586 })587 fastify.inject({588 url: '/',589 method: 'GET'590 }, (e, res) => {591 t.fail('should not be called')592 })593 })594})595 596test('error handler is triggered when a string is thrown from sync handler', t => {597 t.plan(3)598 599 const fastify = Fastify()600 t.teardown(fastify.close.bind(fastify))601 602 const throwable = 'test'603 const payload = 'error'604 605 fastify.get('/', function (req, reply) {606 throw throwable607 })608 609 fastify.setErrorHandler((err, req, res) => {610 t.equal(err, throwable)611 612 res.send(payload)613 })614 615 fastify.inject({616 method: 'GET',617 url: '/'618 }, (err, res) => {619 t.error(err)620 t.equal(res.payload, payload)621 })622})623 624test('status code should be set to 500 and return an error json payload if route handler throws any non Error object expression', async t => {625 t.plan(2)626 const fastify = Fastify()627 t.teardown(fastify.close.bind(fastify))628 629 fastify.get('/', () => {630 /* eslint-disable-next-line */631 throw { foo: 'bar' }632 })633 634 // ----635 const reply = await fastify.inject({ method: 'GET', url: '/' })636 t.equal(reply.statusCode, 500)637 t.equal(JSON.parse(reply.body).foo, 'bar')638})639 640test('should preserve the status code set by the user if an expression is thrown in a sync route', async t => {641 t.plan(2)642 const fastify = Fastify()643 t.teardown(fastify.close.bind(fastify))644 645 fastify.get('/', (_, rep) => {646 rep.status(501)647 648 /* eslint-disable-next-line */649 throw { foo: 'bar' }650 })651 652 // ----653 const reply = await fastify.inject({ method: 'GET', url: '/' })654 t.equal(reply.statusCode, 501)655 t.equal(JSON.parse(reply.body).foo, 'bar')656})657 658test('should trigger error handlers if a sync route throws any non-error object', async t => {659 t.plan(2)660 661 const fastify = Fastify()662 t.teardown(fastify.close.bind(fastify))663 664 const throwable = 'test'665 const payload = 'error'666 667 fastify.get('/', function async (req, reply) {668 throw throwable669 })670 671 fastify.setErrorHandler((err, req, res) => {672 t.equal(err, throwable)673 res.code(500).send(payload)674 })675 676 const reply = await fastify.inject({ method: 'GET', url: '/' })677 t.equal(reply.statusCode, 500)678})679 680test('should trigger error handlers if a sync route throws undefined', async t => {681 t.plan(1)682 683 const fastify = Fastify()684 t.teardown(fastify.close.bind(fastify))685 686 fastify.get('/', function async (req, reply) {687 // eslint-disable-next-line no-throw-literal688 throw undefined689 })690 691 const reply = await fastify.inject({ method: 'GET', url: '/' })692 t.equal(reply.statusCode, 500)693})694 695test('setting content-type on reply object should not hang the server case 1', t => {696 t.plan(2)697 const fastify = Fastify()698 t.teardown(fastify.close.bind(fastify))699 700 fastify.get('/', (req, reply) => {701 reply702 .code(200)703 .headers({ 'content-type': 'text/plain; charset=utf-32' })704 .send(JSON.stringify({ bar: 'foo', baz: 'foobar' }))705 })706 707 fastify.inject({708 url: '/',709 method: 'GET'710 }, (err, res) => {711 t.error(err)712 t.equal(res.statusCode, 200)713 })714})715 716test('setting content-type on reply object should not hang the server case 2', async t => {717 t.plan(1)718 const fastify = Fastify()719 t.teardown(fastify.close.bind(fastify))720 721 fastify.get('/', (req, reply) => {722 reply723 .code(200)724 .headers({ 'content-type': 'text/plain; charset=utf-8' })725 .send({ bar: 'foo', baz: 'foobar' })726 })727 728 try {729 await fastify.ready()730 const res = await fastify.inject({731 url: '/',732 method: 'GET'733 })734 t.same({735 error: 'Internal Server Error',736 message: 'Attempted to send payload of invalid type \'object\'. Expected a string or Buffer.',737 statusCode: 500,738 code: 'FST_ERR_REP_INVALID_PAYLOAD_TYPE'739 },740 res.json())741 } catch (error) {742 t.error(error)743 } finally {744 await fastify.close()745 }746})747 748test('setting content-type on reply object should not hang the server case 3', t => {749 t.plan(2)750 const fastify = Fastify()751 t.teardown(fastify.close.bind(fastify))752 753 fastify.get('/', (req, reply) => {754 reply755 .code(200)756 .headers({ 'content-type': 'application/json' })757 .send({ bar: 'foo', baz: 'foobar' })758 })759 760 fastify.inject({761 url: '/',762 method: 'GET'763 }, (err, res) => {764 t.error(err)765 t.equal(res.statusCode, 200)766 })767})768 769test('pipe stream inside error handler should not cause error', t => {770 t.plan(3)771 const location = path.join(__dirname, '..', 'package.json')772 const json = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString('utf8'))773 774 const fastify = Fastify()775 t.teardown(fastify.close.bind(fastify))776 777 fastify.setErrorHandler((_error, _request, reply) => {778 const stream = fs.createReadStream(location)779 reply.code(400).type('application/json; charset=utf-8').send(stream)780 })781 782 fastify.get('/', (request, reply) => {783 throw new Error('This is an error.')784 })785 786 fastify.inject({787 url: '/',788 method: 'GET'789 }, (err, res) => {790 t.error(err)791 t.equal(res.statusCode, 400)792 t.same(JSON.parse(res.payload), json)793 })794})795 