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/SECURITY.md +6 -0
- package/docs/Guides/Ecosystem.md +8 -53
- package/docs/Reference/Errors.md +0 -2
- package/docs/Reference/Reply.md +17 -0
- package/docs/Reference/Request.md +8 -0
- package/docs/Reference/Routes.md +6 -2
- package/docs/Reference/Server.md +2 -2
- package/docs/Reference/Validation-and-Serialization.md +33 -0
- package/docs/Reference/Warnings.md +2 -0
- package/lib/content-type-parser.js +21 -12
- package/lib/content-type.js +4 -4
- package/lib/errors.js +0 -5
- package/lib/handle-request.js +7 -20
- package/lib/reply.js +6 -2
- package/lib/request.js +9 -5
- package/lib/route.js +6 -2
- package/lib/validation.js +67 -10
- package/lib/warnings.js +10 -1
- package/package.json +1 -1
- package/test/child-logger-factory.test.js +25 -0
- package/test/content-parser.test.js +60 -0
- package/test/internals/errors.test.js +1 -11
- package/test/internals/reply-serialize.test.js +20 -0
- package/test/internals/reply.test.js +30 -1
- package/test/internals/request.test.js +50 -0
- package/test/reply-media-type.test.js +57 -0
- package/test/rfc-10008.test.js +6 -9
- package/test/schema-feature.test.js +1 -1
- package/test/schema-validation.test.js +251 -1
- package/test/types/errors.tst.ts +0 -1
- package/test/types/fastify.tst.ts +5 -0
- package/test/types/route.tst.ts +21 -0
- package/types/errors.d.ts +0 -1
- package/types/instance.d.ts +4 -4
- package/types/route.d.ts +3 -2
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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 =
|
|
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
|
+
})
|
package/test/rfc-10008.test.js
CHANGED
|
@@ -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
|
|
103
|
-
// If a media type is
|
|
104
|
-
//
|
|
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': '
|
|
116
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
120
117
|
})
|
|
121
118
|
|
|
122
|
-
t.assert.equal(res.statusCode,
|
|
123
|
-
t.assert.equal(res.
|
|
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
|
|
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 })
|