fastify 6.0.0-alpha.4 → 6.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/validation.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  FST_ERR_SCH_RESPONSE_SCHEMA_NOT_NESTED_2XX
14
14
  } = require('./errors')
15
15
 
16
- const { FSTWRN001, FSTSEC002 } = require('./warnings')
16
+ const { FSTWRN001, FSTSEC002, FSTSEC003 } = require('./warnings')
17
17
 
18
18
  function compileSchemasForSerialization (context, compile) {
19
19
  if (!context.schema || !context.schema.response) {
@@ -70,9 +70,14 @@ function compileSchemasForSerialization (context, compile) {
70
70
  // reachable through `allOf`, `anyOf`, `oneOf`, `not`, `if`/`then`/`else`, local
71
71
  // `$ref` definitions (`definitions`/`$defs`), and the other standard subschema
72
72
  // containers. The input schema is never mutated: a new schema tree is returned.
73
- function lowerCaseHeadersSchema (schema) {
73
+ //
74
+ // A `propertyNames` subschema validates each header name itself, so values held
75
+ // in `enum` and `const` (and in every subschema reachable from it) are header
76
+ // names rather than header values. `insidePropertyNames` tracks that context so
77
+ // those `enum`/`const` string values are lowercased there, and only there.
78
+ function lowerCaseHeadersSchema (schema, insidePropertyNames = false) {
74
79
  if (Array.isArray(schema)) {
75
- return schema.map(lowerCaseHeadersSchema)
80
+ return schema.map(item => lowerCaseHeadersSchema(item, insidePropertyNames))
76
81
  }
77
82
  if (schema === null || typeof schema !== 'object') {
78
83
  return schema
@@ -91,7 +96,7 @@ function lowerCaseHeadersSchema (schema) {
91
96
  }
92
97
  const normalized = {}
93
98
  for (const prop of Object.keys(value)) {
94
- normalized[prop.toLowerCase()] = lowerCaseHeadersSchema(value[prop])
99
+ normalized[prop.toLowerCase()] = lowerCaseHeadersSchema(value[prop], insidePropertyNames)
95
100
  }
96
101
  result.properties = normalized
97
102
  break
@@ -112,7 +117,7 @@ function lowerCaseHeadersSchema (schema) {
112
117
  if (Array.isArray(depValue)) {
113
118
  normalized[dep.toLowerCase()] = depValue.map(name => name.toLowerCase())
114
119
  } else {
115
- normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(depValue)
120
+ normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(depValue, insidePropertyNames)
116
121
  }
117
122
  }
118
123
  result.dependencies = normalized
@@ -125,7 +130,7 @@ function lowerCaseHeadersSchema (schema) {
125
130
  }
126
131
  const normalized = {}
127
132
  for (const dep of Object.keys(value)) {
128
- normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(value[dep])
133
+ normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(value[dep], insidePropertyNames)
129
134
  }
130
135
  result.dependentSchemas = normalized
131
136
  break
@@ -158,12 +163,21 @@ function lowerCaseHeadersSchema (schema) {
158
163
  case 'unevaluatedItems':
159
164
  case 'unevaluatedProperties':
160
165
  case 'contains':
161
- case 'propertyNames':
162
166
  case 'contentSchema':
163
- result[key] = lowerCaseHeadersSchema(value)
167
+ result[key] = lowerCaseHeadersSchema(value, insidePropertyNames)
168
+ break
169
+ case 'propertyNames':
170
+ result[key] = lowerCaseHeadersSchema(value, true)
164
171
  break
165
172
  // Maps of subschemas whose keys are not header names (definition names,
166
173
  // regex patterns): keep the keys, normalize the subschema values.
174
+ //
175
+ // `patternProperties` keys are regular expressions matched against the
176
+ // already-lowercased header names and are left as-is: a regular expression
177
+ // cannot be safely lowercased. The same applies to a `propertyNames`
178
+ // `pattern`. A header-name pattern written in canonical case will not match
179
+ // the lowercased request headers, so it must be authored to match
180
+ // lowercase. See docs/Reference/Validation-and-Serialization.md.
167
181
  case 'definitions':
168
182
  case '$defs':
169
183
  case 'patternProperties': {
@@ -173,13 +187,23 @@ function lowerCaseHeadersSchema (schema) {
173
187
  }
174
188
  const normalized = {}
175
189
  for (const k of Object.keys(value)) {
176
- normalized[k] = lowerCaseHeadersSchema(value[k])
190
+ normalized[k] = lowerCaseHeadersSchema(value[k], insidePropertyNames)
177
191
  }
178
192
  result[key] = normalized
179
193
  break
180
194
  }
181
195
  default:
182
- result[key] = value
196
+ // Inside a `propertyNames` subschema, `enum` and `const` values are the
197
+ // candidate header names themselves, so they must be lowercased to match
198
+ // the lowercased request headers. Everywhere else these hold header
199
+ // *values* (or arbitrary constant data) and must be left untouched.
200
+ if (insidePropertyNames && key === 'enum' && Array.isArray(value)) {
201
+ result[key] = value.map(item => typeof item === 'string' ? item.toLowerCase() : item)
202
+ } else if (insidePropertyNames && key === 'const' && typeof value === 'string') {
203
+ result[key] = value.toLowerCase()
204
+ } else {
205
+ result[key] = value
206
+ }
183
207
  }
184
208
  }
185
209
 
@@ -217,6 +241,35 @@ function findExternalHeaderRef (schema) {
217
241
  return undefined
218
242
  }
219
243
 
244
+ // A `$ref` reached inside a `propertyNames` subschema resolves to a definition
245
+ // that is normalized in its own, non-`propertyNames` context, so header names
246
+ // held in its `enum`/`const` keep their original case and a case-insensitive
247
+ // deny-list or allow-list never matches the lowercased request headers. We
248
+ // cannot safely normalize the referenced definition here because it may be
249
+ // shared with value contexts, so we detect the reference and warn instead.
250
+ // Returns the first local `$ref` found inside a `propertyNames` subschema, or
251
+ // undefined.
252
+ function findPropertyNamesRef (schema, insidePropertyNames = false) {
253
+ if (Array.isArray(schema)) {
254
+ for (const item of schema) {
255
+ const ref = findPropertyNamesRef(item, insidePropertyNames)
256
+ if (ref !== undefined) return ref
257
+ }
258
+ return undefined
259
+ }
260
+ if (schema === null || typeof schema !== 'object') {
261
+ return undefined
262
+ }
263
+ if (insidePropertyNames && typeof schema.$ref === 'string' && schema.$ref[0] === '#') {
264
+ return schema.$ref
265
+ }
266
+ for (const key of Object.keys(schema)) {
267
+ const ref = findPropertyNamesRef(schema[key], insidePropertyNames || key === 'propertyNames')
268
+ if (ref !== undefined) return ref
269
+ }
270
+ return undefined
271
+ }
272
+
220
273
  function compileSchemasForValidation (context, compile, isCustom) {
221
274
  const { schema } = context
222
275
  if (!schema) {
@@ -246,6 +299,10 @@ function compileSchemasForValidation (context, compile, isCustom) {
246
299
  if (externalRef !== undefined) {
247
300
  FSTSEC002(method, url, externalRef)
248
301
  }
302
+ const propertyNamesRef = findPropertyNamesRef(headers)
303
+ if (propertyNamesRef !== undefined) {
304
+ FSTSEC003(method, url, propertyNamesRef)
305
+ }
249
306
  context[headersSchema] = compile({ schema: headersSchemaLowerCase, method, url, httpPart: 'headers' })
250
307
  }
251
308
  } else if (Object.hasOwn(schema, 'headers')) {
package/lib/warnings.js CHANGED
@@ -7,6 +7,7 @@ const { createWarning } = require('process-warning')
7
7
  * - FSTWRN001
8
8
  * - FSTSEC001
9
9
  * - FSTSEC002
10
+ * - FSTSEC003
10
11
  *
11
12
  * Deprecation Codes FSTDEP001 - FSTDEP021 were used by v4 and MUST NOT be reused.
12
13
  * - FSTDEP022 - FSTDEP025 are used by v5 and MUST NOT be reused.
@@ -41,9 +42,17 @@ const FSTSEC002 = createWarning({
41
42
  unlimited: true
42
43
  })
43
44
 
45
+ const FSTSEC003 = createWarning({
46
+ name: 'FastifySecurity',
47
+ code: 'FSTSEC003',
48
+ message: 'The headers schema for %s: %s uses a $ref (%s) inside propertyNames that is not case-normalized. Header names held in the referenced schema keep their original case and will not match the lowercased request headers, so a case-insensitive deny-list or allow-list on header names may not apply. Inline the propertyNames constraint instead of referencing it with a $ref.',
49
+ unlimited: true
50
+ })
51
+
44
52
  module.exports = {
45
53
  FSTWRN001,
46
54
  FSTWRN003,
47
55
  FSTSEC001,
48
- FSTSEC002
56
+ FSTSEC002,
57
+ FSTSEC003
49
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "6.0.0-alpha.4",
3
+ "version": "6.0.0-alpha.5",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -126,3 +126,28 @@ test('should throw error if invalid logger is returned', (t, done) => {
126
126
  }, { code: 'FST_ERR_LOG_INVALID_LOGGER' })
127
127
  })
128
128
  })
129
+
130
+ test('request child loggers only receive a level when the route sets one', async t => {
131
+ t.plan(3)
132
+
133
+ const fastify = Fastify({ logger: { level: 'info', stream: { write () {} } } })
134
+ const levels = []
135
+ fastify.setChildLoggerFactory(function (logger, bindings, opts) {
136
+ // an empty level would make pino reset the level of every request child
137
+ levels.push(Object.hasOwn(opts, 'level') ? opts.level : 'inherited')
138
+ return logger.child(bindings, opts)
139
+ })
140
+
141
+ fastify.get('/', (req, reply) => {
142
+ reply.send(req.log.level)
143
+ })
144
+ fastify.get('/warn', { logLevel: 'warn' }, (req, reply) => {
145
+ reply.send(req.log.level)
146
+ })
147
+
148
+ t.after(() => fastify.close())
149
+
150
+ t.assert.strictEqual((await fastify.inject('/')).body, 'info')
151
+ t.assert.strictEqual((await fastify.inject('/warn')).body, 'warn')
152
+ t.assert.deepStrictEqual(levels, ['inherited', 'warn'])
153
+ })
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { test } = require('node:test')
4
4
  const { spyWarning } = require('process-warning')
5
+ const { Readable } = require('node:stream')
5
6
  const Fastify = require('..')
6
7
  const keys = require('../lib/symbols')
7
8
  const { FST_ERR_CTP_ALREADY_PRESENT, FST_ERR_CTP_INVALID_TYPE, FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('../lib/errors')
@@ -767,3 +768,62 @@ test('content-type fail when not a valid type', async t => {
767
768
  t.assert.equal(error.message, 'The content type should be a string or a RegExp')
768
769
  }
769
770
  })
771
+
772
+ test('string body keeps multi-byte characters split across chunks', async t => {
773
+ t.plan(2)
774
+
775
+ const fastify = Fastify()
776
+ const encoded = Buffer.from(JSON.stringify({ hello: 'wörld ✓ 😀' }))
777
+
778
+ fastify.addHook('preParsing', async () => {
779
+ // Emit one byte per chunk so every multi-byte character is split
780
+ return Readable.from(Array.from(encoded, byte => Buffer.from([byte])), { objectMode: false })
781
+ })
782
+ fastify.post('/', async (request) => request.body)
783
+
784
+ const res = await fastify.inject({
785
+ method: 'POST',
786
+ url: '/',
787
+ headers: { 'content-type': 'application/json' },
788
+ payload: encoded
789
+ })
790
+ t.assert.strictEqual(res.statusCode, 200)
791
+ t.assert.deepStrictEqual(res.json(), { hello: 'wörld ✓ 😀' })
792
+ })
793
+
794
+ test('string body accepts string chunks from a preParsing stream', async t => {
795
+ t.plan(2)
796
+
797
+ const fastify = Fastify()
798
+
799
+ fastify.addHook('preParsing', async (request, reply, payload) => {
800
+ payload.setEncoding('utf8')
801
+ return payload
802
+ })
803
+ fastify.post('/', async (request) => request.body)
804
+
805
+ const res = await fastify.inject({
806
+ method: 'POST',
807
+ url: '/',
808
+ headers: { 'content-type': 'text/plain' },
809
+ payload: 'hellö'
810
+ })
811
+ t.assert.strictEqual(res.statusCode, 200)
812
+ t.assert.strictEqual(res.body, 'hellö')
813
+ })
814
+
815
+ test('string body with invalid UTF-8 is not treated as a content-length mismatch', async t => {
816
+ t.plan(2)
817
+
818
+ const fastify = Fastify()
819
+ fastify.post('/', async (request) => request.body)
820
+
821
+ const res = await fastify.inject({
822
+ method: 'POST',
823
+ url: '/',
824
+ headers: { 'content-type': 'text/plain' },
825
+ payload: Buffer.from([0x61, 0xff, 0x62])
826
+ })
827
+ t.assert.strictEqual(res.statusCode, 200)
828
+ t.assert.strictEqual(res.body, 'a�b')
829
+ })
@@ -5,7 +5,7 @@ const errors = require('../../lib/errors')
5
5
  const { readFileSync } = require('node:fs')
6
6
  const { resolve } = require('node:path')
7
7
 
8
- const expectedErrors = 95
8
+ const expectedErrors = 94
9
9
 
10
10
  test(`should expose ${expectedErrors} errors`, t => {
11
11
  t.plan(1)
@@ -790,16 +790,6 @@ test('FST_ERR_ROUTE_MISSING_CONTENT_TYPE', t => {
790
790
  t.assert.ok(error instanceof Error)
791
791
  })
792
792
 
793
- test('FST_ERR_ROUTE_MISSING_CONTENT', t => {
794
- t.plan(5)
795
- const error = new errors.FST_ERR_ROUTE_MISSING_CONTENT()
796
- t.assert.strictEqual(error.name, 'FastifyError')
797
- t.assert.strictEqual(error.code, 'FST_ERR_ROUTE_MISSING_CONTENT')
798
- t.assert.strictEqual(error.message, "Method '%s' must provide a request body.")
799
- t.assert.strictEqual(error.statusCode, 400)
800
- t.assert.ok(error instanceof Error)
801
- })
802
-
803
793
  test('FST_ERR_REOPENED_CLOSE_SERVER', t => {
804
794
  t.plan(5)
805
795
  const error = new errors.FST_ERR_REOPENED_CLOSE_SERVER()
@@ -712,3 +712,23 @@ test('Reply#serializeInput', async t => {
712
712
  })
713
713
  })
714
714
  })
715
+
716
+ test('Reply#compileSerializationSchema omits absent metadata instead of passing null', async t => {
717
+ t.plan(2)
718
+
719
+ const fastify = Fastify()
720
+
721
+ fastify.setSerializerCompiler(({ httpStatus, contentType }) => {
722
+ t.assert.strictEqual(httpStatus, undefined)
723
+ t.assert.strictEqual(contentType, undefined)
724
+
725
+ return input => JSON.stringify(input)
726
+ })
727
+
728
+ fastify.get('/', (req, reply) => {
729
+ reply.compileSerializationSchema(getDefaultSchema())
730
+ reply.send({ hello: 'world' })
731
+ })
732
+
733
+ await fastify.inject({ path: '/', method: 'GET' })
734
+ })
@@ -35,7 +35,7 @@ const doGet = async function (url) {
35
35
 
36
36
  test('Once called, Reply should return an object with methods', t => {
37
37
  t.plan(16)
38
- const response = { res: 'res' }
38
+ const response = { res: 'res', getHeader: () => undefined }
39
39
  const context = {
40
40
  config: { onSend: [] },
41
41
  schema: {},
@@ -1003,6 +1003,35 @@ test('reply can set multiple instances of same header', async t => {
1003
1003
  t.assert.deepStrictEqual(result.headers.getSetCookie(), ['one', 'two'])
1004
1004
  })
1005
1005
 
1006
+ test('reply.header does not mutate a set-cookie array passed by the caller', async t => {
1007
+ t.plan(5)
1008
+
1009
+ const fastify = require('../../')()
1010
+ const defaults = ['a=1']
1011
+
1012
+ fastify.get('/headers', function (req, reply) {
1013
+ reply
1014
+ .header('set-cookie', defaults)
1015
+ .header('set-cookie', 'b=2')
1016
+ .send({})
1017
+ })
1018
+
1019
+ const fastifyServer = await fastify.listen({ port: 0 })
1020
+ t.after(() => fastify.close())
1021
+
1022
+ const first = await fetch(`${fastifyServer}/headers`)
1023
+ t.assert.deepStrictEqual(first.headers.getSetCookie(), ['a=1', 'b=2'])
1024
+ const second = await fetch(`${fastifyServer}/headers`)
1025
+ t.assert.deepStrictEqual(second.headers.getSetCookie(), ['a=1', 'b=2'])
1026
+ t.assert.deepStrictEqual(defaults, ['a=1'])
1027
+
1028
+ const reply = new Reply({ setHeader () {}, removeHeader () {} }, {}, {})
1029
+ const cookies = ['one']
1030
+ reply.header('set-cookie', cookies).header('set-cookie', 'two')
1031
+ t.assert.deepStrictEqual(reply.getHeader('set-cookie'), ['one', 'two'])
1032
+ t.assert.deepStrictEqual(cookies, ['one'])
1033
+ })
1034
+
1006
1035
  test('reply.hasHeader returns correct values', async t => {
1007
1036
  t.plan(2)
1008
1037
 
@@ -402,6 +402,56 @@ test('Request with trust proxy - handles multiple entries in x-forwarded-host/pr
402
402
  t.assert.strictEqual(request.protocol, 'https')
403
403
  })
404
404
 
405
+ test('Request with trust proxy - host getter reads the merged headers once', t => {
406
+ t.plan(2)
407
+ const req = {
408
+ method: 'GET',
409
+ url: '/',
410
+ socket: { remoteAddress: 'ip' },
411
+ headers: { host: 'fastify.test' }
412
+ }
413
+
414
+ const TpRequest = Request.buildRequest(Request, true)
415
+ const request = new TpRequest('id', 'params', req, 'query', 'log')
416
+ // the headers getter may allocate a new object once request.headers was
417
+ // assigned in a hook, so the host getter must read it once per access
418
+ const additional = { 'user-assigned': 'yes' }
419
+ let reads = 0
420
+ Object.defineProperty(request, 'headers', {
421
+ get () {
422
+ reads++
423
+ return Object.assign({}, req.headers, additional)
424
+ }
425
+ })
426
+ t.assert.strictEqual(request.host, 'fastify.test')
427
+ t.assert.strictEqual(reads, 1)
428
+ })
429
+
430
+ test('Request with trust proxy - protocol getter reads the merged headers once', t => {
431
+ t.plan(2)
432
+ const req = {
433
+ method: 'GET',
434
+ url: '/',
435
+ socket: { remoteAddress: 'ip' },
436
+ headers: { 'x-forwarded-proto': 'https' }
437
+ }
438
+
439
+ const TpRequest = Request.buildRequest(Request, true)
440
+ const request = new TpRequest('id', 'params', req, 'query', 'log')
441
+ // the headers getter may allocate a new object once request.headers was
442
+ // assigned in a hook, so the protocol getter must read it once per access
443
+ const additional = { 'user-assigned': 'yes' }
444
+ let reads = 0
445
+ Object.defineProperty(request, 'headers', {
446
+ get () {
447
+ reads++
448
+ return Object.assign({}, req.headers, additional)
449
+ }
450
+ })
451
+ t.assert.strictEqual(request.protocol, 'https')
452
+ t.assert.strictEqual(reads, 1)
453
+ })
454
+
405
455
  test('Request with trust proxy - plain', t => {
406
456
  t.plan(1)
407
457
  const headers = {
@@ -129,3 +129,60 @@ test('reply.mediaType should reflect the last type set', async (t) => {
129
129
  const body = response.body
130
130
  t.assert.strictEqual(body, 'mediaType = text/plain')
131
131
  })
132
+
133
+ test('reply.mediaType should match the content-type set via raw.setHeader', async (t) => {
134
+ t.plan(2)
135
+
136
+ const fastify = Fastify()
137
+
138
+ fastify.get('/', (request, reply) => {
139
+ reply.raw.setHeader('content-type', 'application/json; charset=utf-8')
140
+ t.assert.strictEqual(reply.mediaType, 'application/json')
141
+ reply.send({ mediaType: reply.mediaType })
142
+ })
143
+
144
+ const response = await fastify.inject({
145
+ method: 'GET',
146
+ url: '/'
147
+ })
148
+ const body = await response.json()
149
+ t.assert.strictEqual(body.mediaType, 'application/json')
150
+ })
151
+
152
+ test('reply.mediaType should match case-insensitive content-type set via raw.setHeader', async (t) => {
153
+ t.plan(2)
154
+
155
+ const fastify = Fastify()
156
+
157
+ fastify.get('/', (request, reply) => {
158
+ reply.raw.setHeader('Content-Type', 'text/html; charset=utf-8')
159
+ t.assert.strictEqual(reply.mediaType, 'text/html')
160
+ reply.send(reply.mediaType)
161
+ })
162
+
163
+ const response = await fastify.inject({
164
+ method: 'GET',
165
+ url: '/'
166
+ })
167
+ t.assert.strictEqual(response.body, 'text/html')
168
+ })
169
+
170
+ test('reply.mediaType should prioritize reply.type over raw.setHeader', async (t) => {
171
+ t.plan(2)
172
+
173
+ const fastify = Fastify()
174
+
175
+ fastify.get('/', (request, reply) => {
176
+ reply.raw.setHeader('content-type', 'application/xml')
177
+ reply.type('application/json')
178
+ t.assert.strictEqual(reply.mediaType, 'application/json')
179
+ reply.send({ mediaType: reply.mediaType })
180
+ })
181
+
182
+ const response = await fastify.inject({
183
+ method: 'GET',
184
+ url: '/'
185
+ })
186
+ const body = await response.json()
187
+ t.assert.strictEqual(body.mediaType, 'application/json')
188
+ })
@@ -99,12 +99,9 @@ describe('RFC 10008', () => {
99
99
  t.assert.equal(res.json().code, 'FST_ERR_CTP_INVALID_JSON_BODY')
100
100
  })
101
101
 
102
- test('should return 400 if no content is provided for QUERY method', async (t) => {
103
- // If a media type is specified but is inconsistent
104
- // with the actual request content, a 400 (Bad Request)
105
- // can be returned. That is, a server is not allowed to
106
- // infer a media type from the request content and then
107
- // override a missing or "erroneous" value (i.e., "content sniffing").
102
+ test('should return 200 if body is empty but media type allows it for QUERY method', async (t) => {
103
+ // If a media type is set and allows the body to be empty,
104
+ // the server must allow it.
108
105
  t.plan(2)
109
106
 
110
107
  const fastify = Fastify()
@@ -116,11 +113,11 @@ describe('RFC 10008', () => {
116
113
  const res = await fastify.inject({
117
114
  method: 'QUERY',
118
115
  url: '/query',
119
- headers: { 'Content-Type': 'application/json' }
116
+ headers: { 'Content-Type': 'text/plain' }
120
117
  })
121
118
 
122
- t.assert.equal(res.statusCode, 400)
123
- t.assert.equal(res.json().code, 'FST_ERR_ROUTE_MISSING_CONTENT')
119
+ t.assert.equal(res.statusCode, 200)
120
+ t.assert.equal(res.body, '')
124
121
  })
125
122
 
126
123
  test('should return 415 if unsupported media type is provided for QUERY method', async (t) => {
@@ -459,7 +459,7 @@ test('Should emit a warning for every route with undefined schema', (t, testDone
459
459
  t.assert.ifError(error)
460
460
  t.assert.strictEqual(res.statusCode, 200)
461
461
  // fastify.inject run in series
462
- // last callback recieve all warnings at once
462
+ // last callback receive all warnings at once
463
463
  // GET /undefinedParams/123
464
464
  t.assert.deepStrictEqual(spyData.calls[0], { arguments: ['params', 'GET', '/undefinedParams/:id'], result: true })
465
465
  t.assert.deepStrictEqual(spyData.calls[1], { arguments: ['params', 'HEAD', '/undefinedParams/:id'], result: true })