diff --git a/lib/route.js b/lib/route.js index a782c97e85..0e686fc596 100644 --- a/lib/route.js +++ b/lib/route.js @@ -327,7 +327,8 @@ function buildRouting (options) { schemaController.setupValidator(this[kOptions]) } try { - compileSchemasForValidation(context, opts.validatorCompiler || schemaController.validatorCompiler) + const isCustom = typeof opts?.validatorCompiler === 'function' || schemaController.isCustomValidatorCompiler + compileSchemasForValidation(context, opts.validatorCompiler || schemaController.validatorCompiler, isCustom) } catch (error) { throw new FST_ERR_SCH_VALIDATION_BUILD(opts.method, url, error.message) } diff --git a/lib/schema-controller.js b/lib/schema-controller.js index 1484019cb9..78c4437678 100644 --- a/lib/schema-controller.js +++ b/lib/schema-controller.js @@ -25,7 +25,9 @@ function buildSchemaController (parentSchemaCtrl, opts) { const option = { bucket: (opts && opts.bucket) || buildSchemas, - compilersFactory + compilersFactory, + isCustomValidatorCompiler: typeof opts?.compilersFactory?.buildValidator === 'function', + isCustomSerializerCompiler: typeof opts?.compilersFactory?.buildValidator === 'function' } return new SchemaController(undefined, option) @@ -37,6 +39,8 @@ class SchemaController { this.addedSchemas = false this.compilersFactory = this.opts.compilersFactory + this.isCustomValidatorCompiler = this.opts.isCustomValidatorCompiler || false + this.isCustomSerializerCompiler = this.opts.isCustomSerializerCompiler || false if (parent) { this.schemaBucket = this.opts.bucket(parent.getSchemas()) @@ -65,10 +69,12 @@ class SchemaController { // Schema Controller compilers holder setValidatorCompiler (validatorCompiler) { this.validatorCompiler = validatorCompiler + this.isCustomValidatorCompiler = true } setSerializerCompiler (serializerCompiler) { this.serializerCompiler = serializerCompiler + this.isCustomSerializerCompiler = true } getValidatorCompiler () { diff --git a/lib/validation.js b/lib/validation.js index 46ed81c804..939b5c5386 100644 --- a/lib/validation.js +++ b/lib/validation.js @@ -32,7 +32,7 @@ function compileSchemasForSerialization (context, compile) { }, {}) } -function compileSchemasForValidation (context, compile) { +function compileSchemasForValidation (context, compile, isCustom) { const { schema } = context if (!schema) { return @@ -41,8 +41,9 @@ function compileSchemasForValidation (context, compile) { const { method, url } = context.config || {} const headers = schema.headers - if (headers && Object.getPrototypeOf(headers) !== Object.prototype) { - // do not mess with non-literals, e.g. Joi schemas + // the or part is used for backward compatibility + if (headers && (isCustom || Object.getPrototypeOf(headers) !== Object.prototype)) { + // do not mess with schema when custom validator applied, e.g. Joi, Typebox context[headersSchema] = compile({ schema: headers, method, url, httpPart: 'headers' }) } else if (headers) { // The header keys are case insensitive diff --git a/test/internals/validation.test.js b/test/internals/validation.test.js index 320f39bf5d..75e99597ea 100644 --- a/test/internals/validation.test.js +++ b/test/internals/validation.test.js @@ -268,6 +268,21 @@ test('build schema - headers are not lowercased in case of custom object', t => }) }) +test('build schema - headers are not lowercased in case of custom validator provided', t => { + t.plan(1) + + class Headers {} + const opts = { + schema: { + headers: new Headers() + } + } + validation.compileSchemasForValidation(opts, ({ schema, method, url, httpPart }) => { + t.type(schema, Headers) + return () => {} + }, true) +}) + test('build schema - uppercased headers are not included', t => { t.plan(1) const opts = { diff --git a/test/schema-feature.test.js b/test/schema-feature.test.js index af82cd4651..35c9f46861 100644 --- a/test/schema-feature.test.js +++ b/test/schema-feature.test.js @@ -1779,3 +1779,28 @@ test('Should return a human-friendly error if response status codes are not spec t.match(err.message, 'Failed building the serialization schema for GET: /, due to error response schemas should be nested under a valid status code, e.g { 2xx: { type: "object" } }') }) }) + +test('setSchemaController: custom validator instance should not mutate headers schema', async t => { + t.plan(2) + class Headers {} + const fastify = Fastify() + + fastify.setSchemaController({ + compilersFactory: { + buildValidator: function () { + return ({ schema, method, url, httpPart }) => { + t.type(schema, Headers) + return () => {} + } + } + } + }) + + fastify.get('/', { + schema: { + headers: new Headers() + } + }, () => {}) + + await fastify.ready() +}) diff --git a/test/schema-validation.test.js b/test/schema-validation.test.js index 05ce41be5a..e245a17342 100644 --- a/test/schema-validation.test.js +++ b/test/schema-validation.test.js @@ -1017,3 +1017,22 @@ test("The same $id in route's schema must not overwrite others", t => { t.same(res.payload, 'ok') }) }) + +test('Custom validator compiler should not mutate schema', async t => { + t.plan(2) + class Headers {} + const fastify = Fastify() + + fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { + t.type(schema, Headers) + return () => {} + }) + + fastify.get('/', { + schema: { + headers: new Headers() + } + }, () => {}) + + await fastify.ready() +})