fastify 5.7.1 → 5.7.2

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.
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
+ <excludeFolder url="file://$MODULE_DIR$/temp" />
7
+ <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
+ </content>
9
+ <orderEntry type="inheritedJdk" />
10
+ <orderEntry type="sourceFolder" forTests="false" />
11
+ </component>
12
+ </module>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
+ </profile>
6
+ </component>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="JavaScriptLibraryMappings">
4
+ <includedPredefinedLibrary name="Node.js Core" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="EslintConfiguration">
4
+ <option name="fix-on-save" value="true" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/fastify.iml" filepath="$PROJECT_DIR$/.idea/fastify.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
package/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
package/LICENSE CHANGED
@@ -1,9 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2016-present The Fastify Team
4
-
5
- The Fastify team members are listed at https://github.com/fastify/fastify#team
6
- and in the README file.
3
+ Copyright (c) 2016-present The Fastify Team (members are listed in the README file)
7
4
 
8
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
9
6
  of this software and associated documentation files (the "Software"), to deal
package/SPONSORS.md CHANGED
@@ -9,7 +9,7 @@ or [GitHub Sponsors](https://github.com/sponsors/fastify)!
9
9
 
10
10
  ## Tier 4
11
11
 
12
- - [SerpApi](http://serpapi.com/)
12
+ - [SerpApi](https://serpapi.com/?utm_source=fastify)
13
13
 
14
14
  ## Tier 3
15
15
 
@@ -17,7 +17,7 @@ or [GitHub Sponsors](https://github.com/sponsors/fastify)!
17
17
  - [Val Town, Inc.](https://opencollective.com/valtown)
18
18
  - [Handsontable - JavaScript Data Grid](https://handsontable.com/docs/react-data-grid/?utm_source=Fastify_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024)
19
19
  - [Lokalise - A Localization and Translation Software Tool](https://lokalise.com/?utm_source=Fastify_GH&utm_medium=sponsorship)
20
- - [Lambdatest](https://www.lambdatest.com/)
20
+ - [TestMu AI](https://www.testmu.ai/)
21
21
 
22
22
  ## Tier 2
23
23
 
@@ -427,6 +427,8 @@ section.
427
427
  context to take place per API call within the Fastify lifecycle of calls.
428
428
  - [`fastify-http-errors-enhanced`](https://github.com/ShogunPanda/fastify-http-errors-enhanced)
429
429
  An error handling plugin for Fastify that uses enhanced HTTP errors.
430
+ - [`fastify-http-exceptions`](https://github.com/bhouston/fastify-http-exceptions)
431
+ Typed HTTP status exceptions which are automatically converted into Fastify responses.
430
432
  - [`fastify-http2https`](https://github.com/lolo32/fastify-http2https) Redirect
431
433
  HTTP requests to HTTPS, both using the same port number, or different response
432
434
  on HTTP and HTTPS.
@@ -746,7 +748,6 @@ middlewares into Fastify plugins
746
748
  - [`typeorm-fastify-plugin`](https://github.com/jclemens24/fastify-typeorm) A simple
747
749
  and updated Typeorm plugin for use with Fastify.
748
750
 
749
-
750
751
  #### [Community Tools](#community-tools)
751
752
 
752
753
  - [`@fastify-userland/workflows`](https://github.com/fastify-userland/workflows)
@@ -204,12 +204,12 @@ of an *arrow function expression*.
204
204
 
205
205
  You can do the same for the `request` object:
206
206
  ```js
207
- fastify.decorate('getHeader', (req, header) => {
208
- return req.headers[header]
207
+ fastify.decorate('getBoolHeader', (req, name) => {
208
+ return req.headers[name] ?? false // We return `false` if header is missing
209
209
  })
210
210
 
211
211
  fastify.addHook('preHandler', (request, reply, done) => {
212
- request.isHappy = fastify.getHeader(request.raw, 'happy')
212
+ request.isHappy = fastify.getBoolHeader(request, 'happy')
213
213
  done()
214
214
  })
215
215
 
@@ -219,14 +219,14 @@ fastify.get('/happiness', (request, reply) => {
219
219
  ```
220
220
  Again, it works, but it can be much better!
221
221
  ```js
222
- fastify.decorateRequest('setHeader', function (header) {
223
- this.isHappy = this.headers[header]
222
+ fastify.decorateRequest('setBoolHeader', function (name) {
223
+ this.isHappy = this.headers[name] ?? false
224
224
  })
225
225
 
226
226
  fastify.decorateRequest('isHappy', false) // This will be added to the Request object prototype, yay speed!
227
227
 
228
228
  fastify.addHook('preHandler', (request, reply, done) => {
229
- request.setHeader('happy')
229
+ request.setBoolHeader('happy')
230
230
  done()
231
231
  })
232
232
 
@@ -337,11 +337,11 @@ fastify.register((instance, opts, done) => {
337
337
  }
338
338
  })
339
339
 
340
- fastify.get('/plugin1', {config: {useUtil: true}}, (request, reply) => {
340
+ instance.get('/plugin1', {config: {useUtil: true}}, (request, reply) => {
341
341
  reply.send(request)
342
342
  })
343
343
 
344
- fastify.get('/plugin2', (request, reply) => {
344
+ instance.get('/plugin2', (request, reply) => {
345
345
  reply.send(request)
346
346
  })
347
347
 
@@ -534,7 +534,9 @@ recommend using a custom parser to convert only the keys to lowercase.
534
534
  ```js
535
535
  const qs = require('qs')
536
536
  const fastify = require('fastify')({
537
- querystringParser: str => qs.parse(str)
537
+ routerOptions: {
538
+ querystringParser: str => qs.parse(str)
539
+ }
538
540
  })
539
541
  ```
540
542
 
@@ -544,7 +546,9 @@ like the example below for case insensitive keys and values:
544
546
  ```js
545
547
  const querystring = require('fast-querystring')
546
548
  const fastify = require('fastify')({
547
- querystringParser: str => querystring.parse(str.toLowerCase())
549
+ routerOptions: {
550
+ querystringParser: str => querystring.parse(str.toLowerCase())
551
+ }
548
552
  })
549
553
  ```
550
554
 
@@ -675,9 +675,12 @@ const schema = {
675
675
  content: {
676
676
  'application/json': {
677
677
  schema: {
678
- name: { type: 'string' },
679
- image: { type: 'string' },
680
- address: { type: 'string' }
678
+ type: 'object',
679
+ properties: {
680
+ name: { type: 'string' },
681
+ image: { type: 'string' },
682
+ address: { type: 'string' }
683
+ }
681
684
  }
682
685
  },
683
686
  'application/vnd.v1+json': {
@@ -692,8 +695,11 @@ const schema = {
692
695
  content: {
693
696
  'application/vnd.v2+json': {
694
697
  schema: {
695
- fullName: { type: 'string' },
696
- phone: { type: 'string' }
698
+ type: 'object',
699
+ properties: {
700
+ fullName: { type: 'string' },
701
+ phone: { type: 'string' }
702
+ }
697
703
  }
698
704
  }
699
705
  }
@@ -703,7 +709,10 @@ const schema = {
703
709
  // */* is match-all content-type
704
710
  '*/*': {
705
711
  schema: {
706
- desc: { type: 'string' }
712
+ type: 'object',
713
+ properties: {
714
+ desc: { type: 'string' }
715
+ }
707
716
  }
708
717
  }
709
718
  }
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.7.1'
3
+ const VERSION = '5.7.2'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -3,6 +3,7 @@
3
3
  const { AsyncResource } = require('node:async_hooks')
4
4
  const { FifoMap: Fifo } = require('toad-cache')
5
5
  const { parse: secureJsonParse } = require('secure-json-parse')
6
+ const ContentType = require('./content-type')
6
7
  const {
7
8
  kDefaultJsonParse,
8
9
  kContentTypeParser,
@@ -75,8 +76,13 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
75
76
  this.customParsers.set('', parser)
76
77
  } else {
77
78
  if (contentTypeIsString) {
78
- this.parserList.unshift(contentType)
79
- this.customParsers.set(contentType, parser)
79
+ const ct = new ContentType(contentType)
80
+ if (ct.isValid === false) {
81
+ throw new FST_ERR_CTP_INVALID_TYPE()
82
+ }
83
+ const normalizedContentType = ct.toString()
84
+ this.parserList.unshift(normalizedContentType)
85
+ this.customParsers.set(normalizedContentType, parser)
80
86
  } else {
81
87
  validateRegExp(contentType)
82
88
  this.parserRegExpList.unshift(contentType)
@@ -87,7 +93,7 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
87
93
 
88
94
  ContentTypeParser.prototype.hasParser = function (contentType) {
89
95
  if (typeof contentType === 'string') {
90
- contentType = contentType.trim().toLowerCase()
96
+ contentType = new ContentType(contentType).toString()
91
97
  } else {
92
98
  if (!(contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
93
99
  contentType = contentType.toString()
@@ -97,45 +103,49 @@ ContentTypeParser.prototype.hasParser = function (contentType) {
97
103
  }
98
104
 
99
105
  ContentTypeParser.prototype.existingParser = function (contentType) {
100
- if (contentType === 'application/json' && this.customParsers.has(contentType)) {
101
- return this.customParsers.get(contentType).fn !== this[kDefaultJsonParse]
102
- }
103
- if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
104
- return this.customParsers.get(contentType).fn !== defaultPlainTextParser
106
+ if (typeof contentType === 'string') {
107
+ const ct = new ContentType(contentType).toString()
108
+ if (contentType === 'application/json' && this.customParsers.has(contentType)) {
109
+ return this.customParsers.get(ct).fn !== this[kDefaultJsonParse]
110
+ }
111
+ if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
112
+ return this.customParsers.get(ct).fn !== defaultPlainTextParser
113
+ }
105
114
  }
106
115
 
107
116
  return this.hasParser(contentType)
108
117
  }
109
118
 
110
119
  ContentTypeParser.prototype.getParser = function (contentType) {
111
- let parser = this.customParsers.get(contentType)
112
- if (parser !== undefined) return parser
113
- parser = this.cache.get(contentType)
120
+ if (typeof contentType === 'string') {
121
+ contentType = new ContentType(contentType)
122
+ }
123
+ const ct = contentType.toString()
124
+
125
+ let parser = this.cache.get(ct)
114
126
  if (parser !== undefined) return parser
127
+ parser = this.customParsers.get(ct)
128
+ if (parser !== undefined) {
129
+ this.cache.set(ct, parser)
130
+ return parser
131
+ }
115
132
 
116
- const caseInsensitiveContentType = contentType.toLowerCase()
117
- for (let i = 0; i !== this.parserList.length; ++i) {
118
- const parserListItem = this.parserList[i]
119
- if (
120
- caseInsensitiveContentType.slice(0, parserListItem.length) === parserListItem &&
121
- (
122
- caseInsensitiveContentType.length === parserListItem.length ||
123
- caseInsensitiveContentType.charCodeAt(parserListItem.length) === 59 /* `;` */ ||
124
- caseInsensitiveContentType.charCodeAt(parserListItem.length) === 32 /* ` ` */ ||
125
- caseInsensitiveContentType.charCodeAt(parserListItem.length) === 9 /* `\t` */
126
- )
127
- ) {
128
- parser = this.customParsers.get(parserListItem)
129
- this.cache.set(contentType, parser)
130
- return parser
131
- }
133
+ // We have conflicting desires across our test suite. In some cases, we
134
+ // expect to get a parser by just passing the media-type. In others, we expect
135
+ // to get a parser registered under the media-type while also providing
136
+ // parameters. And in yet others, we expect to register a parser under the
137
+ // media-type and have it apply to any request with a header that starts
138
+ // with that type.
139
+ parser = this.customParsers.get(contentType.mediaType)
140
+ if (parser !== undefined) {
141
+ return parser
132
142
  }
133
143
 
134
144
  for (let j = 0; j !== this.parserRegExpList.length; ++j) {
135
145
  const parserRegExp = this.parserRegExpList[j]
136
- if (parserRegExp.test(contentType)) {
146
+ if (parserRegExp.test(ct)) {
137
147
  parser = this.customParsers.get(parserRegExp.toString())
138
- this.cache.set(contentType, parser)
148
+ this.cache.set(ct, parser)
139
149
  return parser
140
150
  }
141
151
  }
@@ -154,7 +164,7 @@ ContentTypeParser.prototype.remove = function (contentType) {
154
164
  let parsers
155
165
 
156
166
  if (typeof contentType === 'string') {
157
- contentType = contentType.trim().toLowerCase()
167
+ contentType = new ContentType(contentType).toString()
158
168
  parsers = this.parserList
159
169
  } else {
160
170
  if (!(contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
@@ -0,0 +1,152 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * keyValuePairsReg is used to split the parameters list into associated
5
+ * key value pairings.
6
+ *
7
+ * @see https://httpwg.org/specs/rfc9110.html#parameter
8
+ * @type {RegExp}
9
+ */
10
+ const keyValuePairsReg = /([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
11
+
12
+ /**
13
+ * typeNameReg is used to validate that the first part of the media-type
14
+ * does not use disallowed characters.
15
+ *
16
+ * @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
17
+ * @type {RegExp}
18
+ */
19
+ const typeNameReg = /^[\w!#$%&'*+.^`|~-]+$/
20
+
21
+ /**
22
+ * subtypeNameReg is used to validate that the second part of the media-type
23
+ * does not use disallowed characters.
24
+ *
25
+ * @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
26
+ * @type {RegExp}
27
+ */
28
+ const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*/
29
+
30
+ /**
31
+ * ContentType parses and represents the value of the content-type header.
32
+ *
33
+ * @see https://httpwg.org/specs/rfc9110.html#media.type
34
+ * @see https://httpwg.org/specs/rfc9110.html#parameter
35
+ */
36
+ class ContentType {
37
+ #valid = false
38
+ #empty = true
39
+ #type = ''
40
+ #subtype = ''
41
+ #parameters = new Map()
42
+ #string
43
+
44
+ constructor (headerValue) {
45
+ if (headerValue == null || headerValue === '' || headerValue === 'undefined') {
46
+ return
47
+ }
48
+
49
+ let sepIdx = headerValue.indexOf(';')
50
+ if (sepIdx === -1) {
51
+ // The value is the simplest `type/subtype` variant.
52
+ sepIdx = headerValue.indexOf('/')
53
+ if (sepIdx === -1) {
54
+ // Got a string without the correct `type/subtype` format.
55
+ return
56
+ }
57
+
58
+ const type = headerValue.slice(0, sepIdx).trimStart().toLowerCase()
59
+ const subtype = headerValue.slice(sepIdx + 1).trimEnd().toLowerCase()
60
+
61
+ if (
62
+ typeNameReg.test(type) === true &&
63
+ subtypeNameReg.test(subtype) === true
64
+ ) {
65
+ this.#valid = true
66
+ this.#empty = false
67
+ this.#type = type
68
+ this.#subtype = subtype
69
+ }
70
+
71
+ return
72
+ }
73
+
74
+ // We have a `type/subtype; params=list...` header value.
75
+ const mediaType = headerValue.slice(0, sepIdx).toLowerCase()
76
+ const paramsList = headerValue.slice(sepIdx + 1).trim()
77
+
78
+ sepIdx = mediaType.indexOf('/')
79
+ if (sepIdx === -1) {
80
+ // We got an invalid string like `something; params=list...`.
81
+ return
82
+ }
83
+ const type = mediaType.slice(0, sepIdx).trimStart()
84
+ const subtype = mediaType.slice(sepIdx + 1).trimEnd()
85
+
86
+ if (
87
+ typeNameReg.test(type) === false ||
88
+ subtypeNameReg.test(subtype) === false
89
+ ) {
90
+ // Some portion of the media-type is using invalid characters. Therefore,
91
+ // the content-type header is invalid.
92
+ return
93
+ }
94
+ this.#type = type
95
+ this.#subtype = subtype
96
+ this.#valid = true
97
+ this.#empty = false
98
+
99
+ let matches = keyValuePairsReg.exec(paramsList)
100
+ while (matches) {
101
+ const key = matches[1]
102
+ const value = matches[2]
103
+ if (value[0] === '"') {
104
+ if (value.at(-1) !== '"') {
105
+ this.#parameters.set(key, 'invalid quoted string')
106
+ matches = keyValuePairsReg.exec(paramsList)
107
+ continue
108
+ }
109
+ // We should probably verify the value matches a quoted string
110
+ // (https://httpwg.org/specs/rfc9110.html#rule.quoted-string) value.
111
+ // But we are not really doing much with the parameter values, so we
112
+ // are omitting that at this time.
113
+ this.#parameters.set(key, value.slice(1, value.length - 1))
114
+ } else {
115
+ this.#parameters.set(key, value)
116
+ }
117
+ matches = keyValuePairsReg.exec(paramsList)
118
+ }
119
+ }
120
+
121
+ get [Symbol.toStringTag] () { return 'ContentType' }
122
+
123
+ get isEmpty () { return this.#empty }
124
+
125
+ get isValid () { return this.#valid }
126
+
127
+ get mediaType () { return `${this.#type}/${this.#subtype}` }
128
+
129
+ get type () { return this.#type }
130
+
131
+ get subtype () { return this.#subtype }
132
+
133
+ get parameters () { return this.#parameters }
134
+
135
+ toString () {
136
+ /* c8 ignore next: we don't need to verify the cache */
137
+ if (this.#string) return this.#string
138
+ const parameters = []
139
+ for (const [key, value] of this.#parameters.entries()) {
140
+ parameters.push(`${key}="${value}"`)
141
+ }
142
+ const result = [this.#type, '/', this.#subtype]
143
+ if (parameters.length > 0) {
144
+ result.push('; ')
145
+ result.push(parameters.join('; '))
146
+ }
147
+ this.#string = result.join('')
148
+ return this.#string
149
+ }
150
+ }
151
+
152
+ module.exports = ContentType
@@ -13,6 +13,20 @@
13
13
  module.exports = function anonymous(validator,serializer
14
14
  ) {
15
15
 
16
+
17
+ const {
18
+ asString,
19
+ asNumber,
20
+ asBoolean,
21
+ asDateTime,
22
+ asDate,
23
+ asTime,
24
+ asUnsafeString
25
+ } = serializer
26
+
27
+ const asInteger = serializer.asInteger.bind(serializer)
28
+
29
+
16
30
  const JSON_STR_BEGIN_OBJECT = '{'
17
31
  const JSON_STR_END_OBJECT = '}'
18
32
  const JSON_STR_BEGIN_ARRAY = '['
@@ -43,7 +57,7 @@ let addComma = false
43
57
  if (value !== undefined) {
44
58
  !addComma && (addComma = true) || (json += JSON_STR_COMMA)
45
59
  json += "\"statusCode\":"
46
- json += serializer.asNumber(value)
60
+ json += asNumber(value)
47
61
  }
48
62
 
49
63
  value = obj["code"]
@@ -57,12 +71,12 @@ let addComma = false
57
71
  } else if (value instanceof Date) {
58
72
  json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
59
73
  } else if (value instanceof RegExp) {
60
- json += serializer.asString(value.source)
74
+ json += asString(value.source)
61
75
  } else {
62
- json += serializer.asString(value.toString())
76
+ json += asString(value.toString())
63
77
  }
64
78
  } else {
65
- json += serializer.asString(value)
79
+ json += asString(value)
66
80
  }
67
81
 
68
82
  }
@@ -78,12 +92,12 @@ let addComma = false
78
92
  } else if (value instanceof Date) {
79
93
  json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
80
94
  } else if (value instanceof RegExp) {
81
- json += serializer.asString(value.source)
95
+ json += asString(value.source)
82
96
  } else {
83
- json += serializer.asString(value.toString())
97
+ json += asString(value.toString())
84
98
  }
85
99
  } else {
86
- json += serializer.asString(value)
100
+ json += asString(value)
87
101
  }
88
102
 
89
103
  }
@@ -99,12 +113,12 @@ let addComma = false
99
113
  } else if (value instanceof Date) {
100
114
  json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
101
115
  } else if (value instanceof RegExp) {
102
- json += serializer.asString(value.source)
116
+ json += asString(value.source)
103
117
  } else {
104
- json += serializer.asString(value.toString())
118
+ json += asString(value.toString())
105
119
  }
106
120
  } else {
107
- json += serializer.asString(value)
121
+ json += asString(value)
108
122
  }
109
123
 
110
124
  }
@@ -1,9 +1,11 @@
1
1
  'use strict'
2
2
 
3
3
  const diagnostics = require('node:diagnostics_channel')
4
+ const ContentType = require('./content-type')
5
+ const wrapThenable = require('./wrap-thenable')
4
6
  const { validate: validateSchema } = require('./validation')
5
7
  const { preValidationHookRunner, preHandlerHookRunner } = require('./hooks')
6
- const wrapThenable = require('./wrap-thenable')
8
+ const { FST_ERR_CTP_INVALID_MEDIA_TYPE } = require('./errors')
7
9
  const { setErrorStatusCode } = require('./error-status')
8
10
  const {
9
11
  kReplyIsError,
@@ -31,9 +33,9 @@ function handleRequest (err, request, reply) {
31
33
 
32
34
  if (this[kSupportedHTTPMethods].bodywith.has(method)) {
33
35
  const headers = request.headers
34
- const contentType = headers['content-type']
36
+ const ctHeader = headers['content-type']
35
37
 
36
- if (contentType === undefined) {
38
+ if (ctHeader === undefined) {
37
39
  const contentLength = headers['content-length']
38
40
  const transferEncoding = headers['transfer-encoding']
39
41
  const isEmptyBody = transferEncoding === undefined &&
@@ -49,7 +51,13 @@ function handleRequest (err, request, reply) {
49
51
  return
50
52
  }
51
53
 
52
- request[kRouteContext].contentTypeParser.run(contentType, handler, request, reply)
54
+ const contentType = new ContentType(ctHeader)
55
+ if (contentType.isValid === false) {
56
+ reply[kReplyIsError] = true
57
+ reply.status(415).send(new FST_ERR_CTP_INVALID_MEDIA_TYPE())
58
+ return
59
+ }
60
+ request[kRouteContext].contentTypeParser.run(contentType.toString(), handler, request, reply)
53
61
  return
54
62
  }
55
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.7.1",
3
+ "version": "5.7.2",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -12,7 +12,7 @@
12
12
  "benchmark:parser:error": "concurrently -k -s first \"node ./examples/benchmark/parser.js\" \"autocannon -c 100 -d 30 -p 10 -i ./examples/benchmark/body.json -H \"content-type:application/jsoff\" -H \"content-length:123\" -m POST localhost:3000/\"",
13
13
  "build:validation": "node build/build-error-serializer.js && node build/build-validation.js",
14
14
  "build:sync-version": "node build/sync-version.js",
15
- "coverage": "c8 --reporter html borp --reporter=@jsumners/line-reporter --coverage --check-coverage --lines 100 ",
15
+ "coverage": "c8 --reporter html borp --reporter=@jsumners/line-reporter",
16
16
  "coverage:ci-check-coverage": "borp --reporter=@jsumners/line-reporter --coverage --check-coverage --lines 100",
17
17
  "lint": "npm run lint:eslint",
18
18
  "lint:fix": "eslint --fix",
@@ -55,6 +55,7 @@ test('getParser', async t => {
55
55
  fastify.addContentTypeParser(/^image\/.*/, first)
56
56
  fastify.addContentTypeParser(/^application\/.+\+xml/, second)
57
57
  fastify.addContentTypeParser('text/html', third)
58
+ fastify.addContentTypeParser('text/html; charset=utf-8', third)
58
59
 
59
60
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('application/t+xml').fn, second)
60
61
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('image/png').fn, first)
@@ -66,14 +67,15 @@ test('getParser', async t => {
66
67
  })
67
68
 
68
69
  await t.test('should return matching parser with caching /1', t => {
69
- t.plan(6)
70
+ t.plan(7)
70
71
 
71
72
  const fastify = Fastify()
72
73
 
73
74
  fastify.addContentTypeParser('text/html', first)
74
75
 
75
- t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)
76
76
  t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 0)
77
+ t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)
78
+ t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)
77
79
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html ').fn, first)
78
80
  t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)
79
81
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html ').fn, first)
@@ -81,20 +83,21 @@ test('getParser', async t => {
81
83
  })
82
84
 
83
85
  await t.test('should return matching parser with caching /2', t => {
84
- t.plan(8)
86
+ t.plan(9)
85
87
 
86
88
  const fastify = Fastify()
87
89
 
88
90
  fastify.addContentTypeParser('text/html', first)
89
91
 
90
- t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)
91
92
  t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 0)
93
+ t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, first)
94
+ t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)
92
95
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/HTML').fn, first)
93
96
  t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)
94
97
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('TEXT/html').fn, first)
95
- t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 2)
98
+ t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)
96
99
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('TEXT/html').fn, first)
97
- t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 2)
100
+ t.assert.strictEqual(fastify[keys.kContentTypeParser].cache.size, 1)
98
101
  })
99
102
 
100
103
  await t.test('should return matching parser with caching /3', t => {
@@ -125,7 +128,7 @@ test('getParser', async t => {
125
128
  })
126
129
 
127
130
  await t.test('should return parser that catches all if no other is set', t => {
128
- t.plan(3)
131
+ t.plan(2)
129
132
 
130
133
  const fastify = Fastify()
131
134
 
@@ -134,7 +137,6 @@ test('getParser', async t => {
134
137
 
135
138
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('image/gif').fn, first)
136
139
  t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text/html').fn, second)
137
- t.assert.strictEqual(fastify[keys.kContentTypeParser].getParser('text').fn, first)
138
140
  })
139
141
 
140
142
  await t.test('should return undefined if no matching parser exist', t => {
@@ -208,7 +210,7 @@ test('add', async t => {
208
210
  const fastify = Fastify()
209
211
  const contentTypeParser = fastify[keys.kContentTypeParser]
210
212
 
211
- t.assert.ifError(contentTypeParser.add('test', {}, first))
213
+ t.assert.ifError(contentTypeParser.add('test/type', {}, first))
212
214
  t.assert.ifError(contentTypeParser.add(/test/, {}, first))
213
215
  t.assert.throws(
214
216
  () => contentTypeParser.add({}, {}, first),
@@ -557,7 +559,7 @@ test('content-type match parameters - regexp', async t => {
557
559
 
558
560
  const fastify = Fastify()
559
561
  fastify.removeAllContentTypeParsers()
560
- fastify.addContentTypeParser(/application\/json; charset=utf8/, function (request, body, done) {
562
+ fastify.addContentTypeParser(/application\/json; charset="utf8"/, function (request, body, done) {
561
563
  t.assert.ok('should be called')
562
564
  done(null, body)
563
565
  })
@@ -697,37 +699,17 @@ test('content-type regexp list should be cloned when plugin override', async t =
697
699
  }
698
700
  })
699
701
 
700
- test('edge case content-type - ;', async t => {
702
+ test('content-type fail when not a valid type', async t => {
701
703
  t.plan(1)
702
704
 
703
705
  const fastify = Fastify()
704
706
  fastify.removeAllContentTypeParsers()
705
- fastify.addContentTypeParser(';', function (request, body, done) {
706
- t.assert.fail('should not be called')
707
- done(null, body)
708
- })
709
-
710
- fastify.post('/', async () => {
711
- return 'ok'
712
- })
713
-
714
- await fastify.inject({
715
- method: 'POST',
716
- path: '/',
717
- headers: {
718
- 'content-type': 'application/json; foo=bar; charset=utf8'
719
- },
720
- body: ''
721
- })
722
-
723
- await fastify.inject({
724
- method: 'POST',
725
- path: '/',
726
- headers: {
727
- 'content-type': 'image/jpeg'
728
- },
729
- body: ''
730
- })
731
-
732
- t.assert.ok('end')
707
+ try {
708
+ fastify.addContentTypeParser('type-only', function (request, body, done) {
709
+ t.assert.fail('shouldn\'t be called')
710
+ done(null, body)
711
+ })
712
+ } catch (error) {
713
+ t.assert.equal(error.message, 'The content type should be a string or a RegExp')
714
+ }
733
715
  })
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const { test } = require('node:test')
3
+ const { describe, test } = require('node:test')
4
+ const ContentType = require('../lib/content-type')
4
5
  const Fastify = require('..')
5
6
 
6
7
  test('should remove content-type for setErrorHandler', async t => {
@@ -40,3 +41,103 @@ test('should remove content-type for setErrorHandler', async t => {
40
41
  t.assert.strictEqual(statusCode, 400)
41
42
  t.assert.strictEqual(body, JSON.stringify({ foo: 'bar' }))
42
43
  })
44
+
45
+ describe('ContentType class', () => {
46
+ test('returns empty instance for empty value', (t) => {
47
+ let found = new ContentType('')
48
+ t.assert.equal(found.isEmpty, true)
49
+
50
+ found = new ContentType('undefined')
51
+ t.assert.equal(found.isEmpty, true)
52
+
53
+ found = new ContentType()
54
+ t.assert.equal(found.isEmpty, true)
55
+ })
56
+
57
+ test('indicates media type is not correct format', (t) => {
58
+ let found = new ContentType('foo')
59
+ t.assert.equal(found.isEmpty, true)
60
+ t.assert.equal(found.isValid, false)
61
+
62
+ found = new ContentType('foo /bar')
63
+ t.assert.equal(found.isEmpty, true)
64
+ t.assert.equal(found.isValid, false)
65
+
66
+ found = new ContentType('foo/ bar')
67
+ t.assert.equal(found.isEmpty, true)
68
+ t.assert.equal(found.isValid, false)
69
+
70
+ found = new ContentType('foo; param=1')
71
+ t.assert.equal(found.isEmpty, true)
72
+ t.assert.equal(found.isValid, false)
73
+
74
+ found = new ContentType('foo/π; param=1')
75
+ t.assert.equal(found.isEmpty, true)
76
+ t.assert.equal(found.isValid, false)
77
+ })
78
+
79
+ test('returns a plain media type instance', (t) => {
80
+ const found = new ContentType('Application/JSON')
81
+ t.assert.equal(found.mediaType, 'application/json')
82
+ t.assert.equal(found.type, 'application')
83
+ t.assert.equal(found.subtype, 'json')
84
+ t.assert.equal(found.parameters.size, 0)
85
+ })
86
+
87
+ test('handles empty parameters list', (t) => {
88
+ const found = new ContentType('Application/JSON ;')
89
+ t.assert.equal(found.isEmpty, false)
90
+ t.assert.equal(found.mediaType, 'application/json')
91
+ t.assert.equal(found.type, 'application')
92
+ t.assert.equal(found.subtype, 'json')
93
+ t.assert.equal(found.parameters.size, 0)
94
+ })
95
+
96
+ test('returns a media type instance with parameters', (t) => {
97
+ const found = new ContentType('Application/JSON ; charset=utf-8; foo=BaR;baz=" 42"')
98
+ t.assert.equal(found.isEmpty, false)
99
+ t.assert.equal(found.mediaType, 'application/json')
100
+ t.assert.equal(found.type, 'application')
101
+ t.assert.equal(found.subtype, 'json')
102
+ t.assert.equal(found.parameters.size, 3)
103
+
104
+ const expected = [
105
+ ['charset', 'utf-8'],
106
+ ['foo', 'BaR'],
107
+ ['baz', ' 42']
108
+ ]
109
+ t.assert.deepStrictEqual(
110
+ Array.from(found.parameters.entries()),
111
+ expected
112
+ )
113
+
114
+ t.assert.equal(
115
+ found.toString(),
116
+ 'application/json; charset="utf-8"; foo="BaR"; baz=" 42"'
117
+ )
118
+ })
119
+
120
+ test('skips invalid quoted string parameters', (t) => {
121
+ const found = new ContentType('Application/JSON ; charset=utf-8; foo=BaR;baz=" 42')
122
+ t.assert.equal(found.isEmpty, false)
123
+ t.assert.equal(found.mediaType, 'application/json')
124
+ t.assert.equal(found.type, 'application')
125
+ t.assert.equal(found.subtype, 'json')
126
+ t.assert.equal(found.parameters.size, 3)
127
+
128
+ const expected = [
129
+ ['charset', 'utf-8'],
130
+ ['foo', 'BaR'],
131
+ ['baz', 'invalid quoted string']
132
+ ]
133
+ t.assert.deepStrictEqual(
134
+ Array.from(found.parameters.entries()),
135
+ expected
136
+ )
137
+
138
+ t.assert.equal(
139
+ found.toString(),
140
+ 'application/json; charset="utf-8"; foo="BaR"; baz="invalid quoted string"'
141
+ )
142
+ })
143
+ })
@@ -354,7 +354,7 @@ test('catch all content type parser', async (t) => {
354
354
  method: 'POST',
355
355
  body: 'hello',
356
356
  headers: {
357
- 'Content-Type': 'very-weird-content-type'
357
+ 'Content-Type': 'very-weird-content-type/foo'
358
358
  }
359
359
  })
360
360
 
@@ -363,7 +363,7 @@ test('catch all content type parser', async (t) => {
363
363
  t.assert.strictEqual(await result2.text(), 'hello')
364
364
  })
365
365
 
366
- test('catch all content type parser should not interfere with other conte type parsers', async (t) => {
366
+ test('catch all content type parser should not interfere with other content type parsers', async (t) => {
367
367
  t.plan(6)
368
368
  const fastify = Fastify()
369
369
 
@@ -404,7 +404,7 @@ test('catch all content type parser should not interfere with other conte type p
404
404
  method: 'POST',
405
405
  body: 'hello',
406
406
  headers: {
407
- 'Content-Type': 'very-weird-content-type'
407
+ 'Content-Type': 'very-weird-content-type/foo'
408
408
  }
409
409
  })
410
410
 
@@ -89,41 +89,6 @@ test('Should get the body as string /1', async (t) => {
89
89
  t.assert.strictEqual(await result.text(), 'hello world')
90
90
  })
91
91
 
92
- test('Should get the body as string /2', async (t) => {
93
- t.plan(4)
94
- const fastify = Fastify()
95
-
96
- fastify.post('/', (req, reply) => {
97
- reply.send(req.body)
98
- })
99
-
100
- fastify.addContentTypeParser('text/plain/test', { parseAs: 'string' }, function (req, body, done) {
101
- t.assert.ok('called')
102
- t.assert.ok(typeof body === 'string')
103
- try {
104
- const plainText = body
105
- done(null, plainText)
106
- } catch (err) {
107
- err.statusCode = 400
108
- done(err, undefined)
109
- }
110
- })
111
-
112
- const fastifyServer = await fastify.listen({ port: 0 })
113
- t.after(() => fastify.close())
114
-
115
- const result = await fetch(fastifyServer, {
116
- method: 'POST',
117
- body: 'hello world',
118
- headers: {
119
- 'Content-Type': ' text/plain/test '
120
- }
121
- })
122
-
123
- t.assert.strictEqual(result.status, 200)
124
- t.assert.strictEqual(await result.text(), 'hello world')
125
- })
126
-
127
92
  test('Should get the body as buffer', async (t) => {
128
93
  t.plan(4)
129
94
  const fastify = Fastify()
@@ -189,7 +189,7 @@ test('catch all content type parser should not interfere with content type parse
189
189
 
190
190
  const assertions = [
191
191
  { body: '{"myKey":"myValue"}', contentType: 'application/json', expected: JSON.stringify({ myKey: 'myValue' }) },
192
- { body: 'body', contentType: 'very-weird-content-type', expected: 'body' },
192
+ { body: 'body', contentType: 'very-weird-content-type/foo', expected: 'body' },
193
193
  { body: 'my text', contentType: 'text/html', expected: 'my texthtml' }
194
194
  ]
195
195
 
@@ -1416,7 +1416,7 @@ test('Schema validation will not be bypass by different content type', async t =
1416
1416
  t.after(() => fastify.close())
1417
1417
  const address = fastify.listeningOrigin
1418
1418
 
1419
- const correct1 = await fetch(address, {
1419
+ let found = await fetch(address, {
1420
1420
  method: 'POST',
1421
1421
  url: '/',
1422
1422
  headers: {
@@ -1424,10 +1424,10 @@ test('Schema validation will not be bypass by different content type', async t =
1424
1424
  },
1425
1425
  body: JSON.stringify({ foo: 'string' })
1426
1426
  })
1427
- t.assert.strictEqual(correct1.status, 200)
1428
- await correct1.bytes()
1427
+ t.assert.strictEqual(found.status, 200)
1428
+ await found.bytes()
1429
1429
 
1430
- const correct2 = await fetch(address, {
1430
+ found = await fetch(address, {
1431
1431
  method: 'POST',
1432
1432
  url: '/',
1433
1433
  headers: {
@@ -1435,10 +1435,10 @@ test('Schema validation will not be bypass by different content type', async t =
1435
1435
  },
1436
1436
  body: JSON.stringify({ foo: 'string' })
1437
1437
  })
1438
- t.assert.strictEqual(correct2.status, 200)
1439
- await correct2.bytes()
1438
+ t.assert.strictEqual(found.status, 200)
1439
+ await found.bytes()
1440
1440
 
1441
- const correct3 = await fetch(address, {
1441
+ found = await fetch(address, {
1442
1442
  method: 'POST',
1443
1443
  url: '/',
1444
1444
  headers: {
@@ -1446,10 +1446,10 @@ test('Schema validation will not be bypass by different content type', async t =
1446
1446
  },
1447
1447
  body: JSON.stringify({ foo: 'string' })
1448
1448
  })
1449
- t.assert.strictEqual(correct2.status, 200)
1450
- await correct3.bytes()
1449
+ t.assert.strictEqual(found.status, 200)
1450
+ await found.bytes()
1451
1451
 
1452
- const invalid1 = await fetch(address, {
1452
+ found = await fetch(address, {
1453
1453
  method: 'POST',
1454
1454
  url: '/',
1455
1455
  headers: {
@@ -1457,10 +1457,10 @@ test('Schema validation will not be bypass by different content type', async t =
1457
1457
  },
1458
1458
  body: JSON.stringify({ invalid: 'string' })
1459
1459
  })
1460
- t.assert.strictEqual(invalid1.status, 400)
1461
- t.assert.strictEqual((await invalid1.json()).code, 'FST_ERR_VALIDATION')
1460
+ t.assert.strictEqual(found.status, 400)
1461
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_VALIDATION')
1462
1462
 
1463
- const invalid2 = await fetch(address, {
1463
+ found = await fetch(address, {
1464
1464
  method: 'POST',
1465
1465
  url: '/',
1466
1466
  headers: {
@@ -1468,10 +1468,10 @@ test('Schema validation will not be bypass by different content type', async t =
1468
1468
  },
1469
1469
  body: JSON.stringify({ invalid: 'string' })
1470
1470
  })
1471
- t.assert.strictEqual(invalid2.status, 400)
1472
- t.assert.strictEqual((await invalid2.json()).code, 'FST_ERR_VALIDATION')
1471
+ t.assert.strictEqual(found.status, 400)
1472
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_VALIDATION')
1473
1473
 
1474
- const invalid3 = await fetch(address, {
1474
+ found = await fetch(address, {
1475
1475
  method: 'POST',
1476
1476
  url: '/',
1477
1477
  headers: {
@@ -1479,10 +1479,10 @@ test('Schema validation will not be bypass by different content type', async t =
1479
1479
  },
1480
1480
  body: JSON.stringify({ invalid: 'string' })
1481
1481
  })
1482
- t.assert.strictEqual(invalid3.status, 400)
1483
- t.assert.strictEqual((await invalid3.json()).code, 'FST_ERR_VALIDATION')
1482
+ t.assert.strictEqual(found.status, 400)
1483
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_VALIDATION')
1484
1484
 
1485
- const invalid4 = await fetch(address, {
1485
+ found = await fetch(address, {
1486
1486
  method: 'POST',
1487
1487
  url: '/',
1488
1488
  headers: {
@@ -1490,10 +1490,10 @@ test('Schema validation will not be bypass by different content type', async t =
1490
1490
  },
1491
1491
  body: JSON.stringify({ invalid: 'string' })
1492
1492
  })
1493
- t.assert.strictEqual(invalid4.status, 400)
1494
- t.assert.strictEqual((await invalid4.json()).code, 'FST_ERR_VALIDATION')
1493
+ t.assert.strictEqual(found.status, 415)
1494
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1495
1495
 
1496
- const invalid5 = await fetch(address, {
1496
+ found = await fetch(address, {
1497
1497
  method: 'POST',
1498
1498
  url: '/',
1499
1499
  headers: {
@@ -1501,10 +1501,10 @@ test('Schema validation will not be bypass by different content type', async t =
1501
1501
  },
1502
1502
  body: JSON.stringify({ invalid: 'string' })
1503
1503
  })
1504
- t.assert.strictEqual(invalid5.status, 400)
1505
- t.assert.strictEqual((await invalid5.json()).code, 'FST_ERR_VALIDATION')
1504
+ t.assert.strictEqual(found.status, 415)
1505
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1506
1506
 
1507
- const invalid6 = await fetch(address, {
1507
+ found = await fetch(address, {
1508
1508
  method: 'POST',
1509
1509
  url: '/',
1510
1510
  headers: {
@@ -1512,10 +1512,10 @@ test('Schema validation will not be bypass by different content type', async t =
1512
1512
  },
1513
1513
  body: JSON.stringify({ invalid: 'string' })
1514
1514
  })
1515
- t.assert.strictEqual(invalid6.status, 400)
1516
- t.assert.strictEqual((await invalid6.json()).code, 'FST_ERR_VALIDATION')
1515
+ t.assert.strictEqual(found.status, 415)
1516
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1517
1517
 
1518
- const invalid7 = await fetch(address, {
1518
+ found = await fetch(address, {
1519
1519
  method: 'POST',
1520
1520
  url: '/',
1521
1521
  headers: {
@@ -1523,10 +1523,10 @@ test('Schema validation will not be bypass by different content type', async t =
1523
1523
  },
1524
1524
  body: JSON.stringify({ invalid: 'string' })
1525
1525
  })
1526
- t.assert.strictEqual(invalid7.status, 400)
1527
- t.assert.strictEqual((await invalid7.json()).code, 'FST_ERR_VALIDATION')
1526
+ t.assert.strictEqual(found.status, 400)
1527
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_VALIDATION')
1528
1528
 
1529
- const invalid8 = await fetch(address, {
1529
+ found = await fetch(address, {
1530
1530
  method: 'POST',
1531
1531
  url: '/',
1532
1532
  headers: {
@@ -1534,6 +1534,39 @@ test('Schema validation will not be bypass by different content type', async t =
1534
1534
  },
1535
1535
  body: JSON.stringify({ invalid: 'string' })
1536
1536
  })
1537
- t.assert.strictEqual(invalid8.status, 400)
1538
- t.assert.strictEqual((await invalid8.json()).code, 'FST_ERR_VALIDATION')
1537
+ t.assert.strictEqual(found.status, 400)
1538
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_VALIDATION')
1539
+
1540
+ found = await fetch(address, {
1541
+ method: 'POST',
1542
+ url: '/',
1543
+ headers: {
1544
+ 'content-type': 'ApPlIcAtIoN/JsOn\ta'
1545
+ },
1546
+ body: JSON.stringify({ invalid: 'string' })
1547
+ })
1548
+ t.assert.strictEqual(found.status, 415)
1549
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1550
+
1551
+ found = await fetch(address, {
1552
+ method: 'POST',
1553
+ url: '/',
1554
+ headers: {
1555
+ 'content-type': 'ApPlIcAtIoN/JsOn\ta; charset=utf-8'
1556
+ },
1557
+ body: JSON.stringify({ invalid: 'string' })
1558
+ })
1559
+ t.assert.strictEqual(found.status, 415)
1560
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1561
+
1562
+ found = await fetch(address, {
1563
+ method: 'POST',
1564
+ url: '/',
1565
+ headers: {
1566
+ 'content-type': 'application/ json'
1567
+ },
1568
+ body: JSON.stringify({ invalid: 'string' })
1569
+ })
1570
+ t.assert.strictEqual(found.status, 415)
1571
+ t.assert.strictEqual((await found.json()).code, 'FST_ERR_CTP_INVALID_MEDIA_TYPE')
1539
1572
  })