From 4e797257001cce8c00a627b8c45f111f365b06a3 Mon Sep 17 00:00:00 2001 From: Philipp Viereck <105976309+philippviereck@users.noreply.github.com> Date: Thu, 11 Aug 2022 14:51:37 +0200 Subject: [PATCH] feat: disable/ignore request-id header (#4193) * add option to disable/ignore request-id header fixes #4192 * add more restrictive schema definition * update configValidator generated by running 'node build/build-validation.js' after change on 'build-validation.js' * move logic into reqIdGenFactory * update docs for requestHeaderId Adding documentation for opt-out of 'requestHeaderId' --- build/build-validation.js | 2 +- docs/Reference/Server.md | 13 +++- fastify.d.ts | 2 +- fastify.js | 4 +- lib/configValidator.js | 87 +++++++++++++++++++++------ lib/reqIdGenFactory.js | 15 ++++- lib/route.js | 4 +- test/logger.test.js | 108 ++++++++++++++++++++++++++++++++++ test/types/fastify.test-d.ts | 1 + test/types/instance.test-d.ts | 2 +- types/instance.d.ts | 2 +- 11 files changed, 207 insertions(+), 33 deletions(-) diff --git a/build/build-validation.js b/build/build-validation.js index 5439355ffe..2ed914f69b 100644 --- a/build/build-validation.js +++ b/build/build-validation.js @@ -97,7 +97,7 @@ const schema = { onProtoPoisoning: { type: 'string', default: defaultInitOptions.onProtoPoisoning }, onConstructorPoisoning: { type: 'string', default: defaultInitOptions.onConstructorPoisoning }, pluginTimeout: { type: 'integer', default: defaultInitOptions.pluginTimeout }, - requestIdHeader: { type: 'string', default: defaultInitOptions.requestIdHeader }, + requestIdHeader: { anyOf: [{ enum: [false] }, { type: 'string' }], default: defaultInitOptions.requestIdHeader }, requestIdLogLabel: { type: 'string', default: defaultInitOptions.requestIdLogLabel }, http2SessionTimeout: { type: 'integer', default: defaultInitOptions.http2SessionTimeout }, exposeHeadRoutes: { type: 'boolean', default: defaultInitOptions.exposeHeadRoutes }, diff --git a/docs/Reference/Server.md b/docs/Reference/Server.md index 694f03e6b3..ed6cd73ac7 100644 --- a/docs/Reference/Server.md +++ b/docs/Reference/Server.md @@ -55,7 +55,7 @@ describes the properties available in that options object. - [routing](#routing) - [route](#route) - [close](#close) - - [decorate\*](#decorate) + - [decorate*](#decorate) - [register](#register) - [addHook](#addhook) - [prefix](#prefix) @@ -488,11 +488,18 @@ about safe regexp: [Safe-regex2](https://www.npmjs.com/package/safe-regex2) ### `requestIdHeader` -The header name used to know the request-id. See [the +The header name used to set the request-id. See [the request-id](./Logging.md#logging-request-id) section. +Setting `requestIdHeader` to `false` will always use [genReqId](#genreqid) + Default: `'request-id'` - + +```js +const fastify = require('fastify')({ + requestIdHeader: 'x-custom-id', // -> use 'X-Custom-Id' header if available + //requestIdHeader: false, // -> always use genReqId +}) +``` ### `requestIdLogLabel` diff --git a/fastify.d.ts b/fastify.d.ts index 66312159d7..c708e179e8 100644 --- a/fastify.d.ts +++ b/fastify.d.ts @@ -123,7 +123,7 @@ export type FastifyServerOptions< serializerOpts?: FJSOptions | Record, serverFactory?: FastifyServerFactory, caseSensitive?: boolean, - requestIdHeader?: string, + requestIdHeader?: string | false, requestIdLogLabel?: string; jsonShorthand?: boolean; genReqId?: (req: FastifyRequest, FastifySchema, TypeProvider>) => string, diff --git a/fastify.js b/fastify.js index 7e56a70500..67509da5b1 100644 --- a/fastify.js +++ b/fastify.js @@ -98,8 +98,8 @@ function fastify (options) { validateBodyLimitOption(options.bodyLimit) - const requestIdHeader = options.requestIdHeader || defaultInitOptions.requestIdHeader - const genReqId = options.genReqId || reqIdGenFactory() + const requestIdHeader = (options.requestIdHeader === false) ? false : (options.requestIdHeader || defaultInitOptions.requestIdHeader) + const genReqId = reqIdGenFactory(requestIdHeader, options.genReqId) const requestIdLogLabel = options.requestIdLogLabel || 'reqId' const bodyLimit = options.bodyLimit || defaultInitOptions.bodyLimit const disableRequestLogging = options.disableRequestLogging || false diff --git a/lib/configValidator.js b/lib/configValidator.js index 8d0e08715c..f7ded6173c 100644 --- a/lib/configValidator.js +++ b/lib/configValidator.js @@ -3,7 +3,7 @@ "use strict"; module.exports = validate10; module.exports.default = validate10; -const schema11 = {"type":"object","additionalProperties":false,"properties":{"connectionTimeout":{"type":"integer","default":0},"keepAliveTimeout":{"type":"integer","default":72000},"forceCloseConnections":{"oneOf":[{"type":"string","pattern":"idle"},{"type":"boolean"}]},"maxRequestsPerSocket":{"type":"integer","default":0,"nullable":true},"requestTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"caseSensitive":{"type":"boolean","default":true},"allowUnsafeRegex":{"type":"boolean","default":false},"http2":{"type":"boolean"},"https":{"if":{"not":{"oneOf":[{"type":"boolean"},{"type":"null"},{"type":"object","additionalProperties":false,"required":["allowHTTP1"],"properties":{"allowHTTP1":{"type":"boolean"}}}]}},"then":{"setDefaultValue":true}},"ignoreTrailingSlash":{"type":"boolean","default":false},"ignoreDuplicateSlashes":{"type":"boolean","default":false},"disableRequestLogging":{"type":"boolean","default":false},"jsonShorthand":{"type":"boolean","default":true},"maxParamLength":{"type":"integer","default":100},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"type":"string","default":"request-id"},"requestIdLogLabel":{"type":"string","default":"reqId"},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"versioning":{"type":"object","additionalProperties":true,"required":["storage","deriveVersion"],"properties":{"storage":{},"deriveVersion":{}}},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}}}}; +const schema11 = {"type":"object","additionalProperties":false,"properties":{"connectionTimeout":{"type":"integer","default":0},"keepAliveTimeout":{"type":"integer","default":72000},"forceCloseConnections":{"oneOf":[{"type":"string","pattern":"idle"},{"type":"boolean"}]},"maxRequestsPerSocket":{"type":"integer","default":0,"nullable":true},"requestTimeout":{"type":"integer","default":0},"bodyLimit":{"type":"integer","default":1048576},"caseSensitive":{"type":"boolean","default":true},"allowUnsafeRegex":{"type":"boolean","default":false},"http2":{"type":"boolean"},"https":{"if":{"not":{"oneOf":[{"type":"boolean"},{"type":"null"},{"type":"object","additionalProperties":false,"required":["allowHTTP1"],"properties":{"allowHTTP1":{"type":"boolean"}}}]}},"then":{"setDefaultValue":true}},"ignoreTrailingSlash":{"type":"boolean","default":false},"ignoreDuplicateSlashes":{"type":"boolean","default":false},"disableRequestLogging":{"type":"boolean","default":false},"jsonShorthand":{"type":"boolean","default":true},"maxParamLength":{"type":"integer","default":100},"onProtoPoisoning":{"type":"string","default":"error"},"onConstructorPoisoning":{"type":"string","default":"error"},"pluginTimeout":{"type":"integer","default":10000},"requestIdHeader":{"anyOf":[{"enum":[false]},{"type":"string"}],"default":"request-id"},"requestIdLogLabel":{"type":"string","default":"reqId"},"http2SessionTimeout":{"type":"integer","default":72000},"exposeHeadRoutes":{"type":"boolean","default":true},"versioning":{"type":"object","additionalProperties":true,"required":["storage","deriveVersion"],"properties":{"storage":{},"deriveVersion":{}}},"constraints":{"type":"object","additionalProperties":{"type":"object","required":["name","storage","validate","deriveConstraint"],"additionalProperties":true,"properties":{"name":{"type":"string"},"storage":{},"validate":{},"deriveConstraint":{}}}}}}; const func2 = Object.prototype.hasOwnProperty; const pattern0 = new RegExp("idle", "u"); @@ -837,6 +837,23 @@ var valid0 = _errs55 === errors; if(valid0){ let data19 = data.requestIdHeader; const _errs57 = errors; +const _errs58 = errors; +let valid6 = false; +const _errs59 = errors; +if(!(data19 === false)){ +const err12 = {instancePath:instancePath+"/requestIdHeader",schemaPath:"#/properties/requestIdHeader/anyOf/0/enum",keyword:"enum",params:{allowedValues: schema11.properties.requestIdHeader.anyOf[0].enum},message:"must be equal to one of the allowed values"}; +if(vErrors === null){ +vErrors = [err12]; +} +else { +vErrors.push(err12); +} +errors++; +} +var _valid3 = _errs59 === errors; +valid6 = valid6 || _valid3; +if(!valid6){ +const _errs60 = errors; if(typeof data19 !== "string"){ let dataType21 = typeof data19; let coerced21 = undefined; @@ -848,8 +865,14 @@ else if(data19 === null){ coerced21 = ""; } else { -validate10.errors = [{instancePath:instancePath+"/requestIdHeader",schemaPath:"#/properties/requestIdHeader/type",keyword:"type",params:{type: "string"},message:"must be string"}]; -return false; +const err13 = {instancePath:instancePath+"/requestIdHeader",schemaPath:"#/properties/requestIdHeader/anyOf/1/type",keyword:"type",params:{type: "string"},message:"must be string"}; +if(vErrors === null){ +vErrors = [err13]; +} +else { +vErrors.push(err13); +} +errors++; } } if(coerced21 !== undefined){ @@ -859,10 +882,36 @@ data["requestIdHeader"] = coerced21; } } } +var _valid3 = _errs60 === errors; +valid6 = valid6 || _valid3; +} +if(!valid6){ +const err14 = {instancePath:instancePath+"/requestIdHeader",schemaPath:"#/properties/requestIdHeader/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf"}; +if(vErrors === null){ +vErrors = [err14]; +} +else { +vErrors.push(err14); +} +errors++; +validate10.errors = vErrors; +return false; +} +else { +errors = _errs58; +if(vErrors !== null){ +if(_errs58){ +vErrors.length = _errs58; +} +else { +vErrors = null; +} +} +} var valid0 = _errs57 === errors; if(valid0){ let data20 = data.requestIdLogLabel; -const _errs59 = errors; +const _errs62 = errors; if(typeof data20 !== "string"){ let dataType22 = typeof data20; let coerced22 = undefined; @@ -885,10 +934,10 @@ data["requestIdLogLabel"] = coerced22; } } } -var valid0 = _errs59 === errors; +var valid0 = _errs62 === errors; if(valid0){ let data21 = data.http2SessionTimeout; -const _errs61 = errors; +const _errs64 = errors; if(!(((typeof data21 == "number") && (!(data21 % 1) && !isNaN(data21))) && (isFinite(data21)))){ let dataType23 = typeof data21; let coerced23 = undefined; @@ -909,10 +958,10 @@ data["http2SessionTimeout"] = coerced23; } } } -var valid0 = _errs61 === errors; +var valid0 = _errs64 === errors; if(valid0){ let data22 = data.exposeHeadRoutes; -const _errs63 = errors; +const _errs66 = errors; if(typeof data22 !== "boolean"){ let coerced24 = undefined; if(!(coerced24 !== undefined)){ @@ -934,12 +983,12 @@ data["exposeHeadRoutes"] = coerced24; } } } -var valid0 = _errs63 === errors; +var valid0 = _errs66 === errors; if(valid0){ if(data.versioning !== undefined){ let data23 = data.versioning; -const _errs65 = errors; -if(errors === _errs65){ +const _errs68 = errors; +if(errors === _errs68){ if(data23 && typeof data23 == "object" && !Array.isArray(data23)){ let missing1; if(((data23.storage === undefined) && (missing1 = "storage")) || ((data23.deriveVersion === undefined) && (missing1 = "deriveVersion"))){ @@ -952,7 +1001,7 @@ validate10.errors = [{instancePath:instancePath+"/versioning",schemaPath:"#/prop return false; } } -var valid0 = _errs65 === errors; +var valid0 = _errs68 === errors; } else { var valid0 = true; @@ -960,13 +1009,13 @@ var valid0 = true; if(valid0){ if(data.constraints !== undefined){ let data24 = data.constraints; -const _errs68 = errors; -if(errors === _errs68){ +const _errs71 = errors; +if(errors === _errs71){ if(data24 && typeof data24 == "object" && !Array.isArray(data24)){ for(const key2 in data24){ let data25 = data24[key2]; -const _errs71 = errors; -if(errors === _errs71){ +const _errs74 = errors; +if(errors === _errs74){ if(data25 && typeof data25 == "object" && !Array.isArray(data25)){ let missing2; if(((((data25.name === undefined) && (missing2 = "name")) || ((data25.storage === undefined) && (missing2 = "storage"))) || ((data25.validate === undefined) && (missing2 = "validate"))) || ((data25.deriveConstraint === undefined) && (missing2 = "deriveConstraint"))){ @@ -1006,8 +1055,8 @@ validate10.errors = [{instancePath:instancePath+"/constraints/" + key2.replace(/ return false; } } -var valid6 = _errs71 === errors; -if(!valid6){ +var valid7 = _errs74 === errors; +if(!valid7){ break; } } @@ -1017,7 +1066,7 @@ validate10.errors = [{instancePath:instancePath+"/constraints",schemaPath:"#/pro return false; } } -var valid0 = _errs68 === errors; +var valid0 = _errs71 === errors; } else { var valid0 = true; diff --git a/lib/reqIdGenFactory.js b/lib/reqIdGenFactory.js index fc7e35ecdb..c8c36d0bea 100644 --- a/lib/reqIdGenFactory.js +++ b/lib/reqIdGenFactory.js @@ -1,6 +1,6 @@ 'use strict' -module.exports = function () { +module.exports = function (requestIdHeader, optGenReqId) { // 2,147,483,647 (2^31 − 1) stands for max SMI value (an internal optimization of V8). // With this upper bound, if you'll be generating 1k ids/sec, you're going to hit it in ~25 days. // This is very likely to happen in real-world applications, hence the limit is enforced. @@ -8,8 +8,19 @@ module.exports = function () { // In the worst cases, it will become a float, losing accuracy. const maxInt = 2147483647 let nextReqId = 0 - return function genReqId (req) { + function defaultGenReqId (req) { nextReqId = (nextReqId + 1) & maxInt return `req-${nextReqId.toString(36)}` } + + const genReqId = optGenReqId || defaultGenReqId + + if (requestIdHeader) { + // requestIdHeader = typeof requestIdHeader === 'string' ? requestIdHeader : 'request-id' + return function (req) { + return req.headers[requestIdHeader] || genReqId(req) + } + } + + return genReqId } diff --git a/lib/route.js b/lib/route.js index 7559653c6c..ef9f920972 100644 --- a/lib/route.js +++ b/lib/route.js @@ -46,7 +46,6 @@ function buildRouting (options) { let avvio let fourOhFour - let requestIdHeader let requestIdLogLabel let logger let hasLogger @@ -74,7 +73,6 @@ function buildRouting (options) { validateHTTPVersion = fastifyArgs.validateHTTPVersion globalExposeHeadRoutes = options.exposeHeadRoutes - requestIdHeader = options.requestIdHeader requestIdLogLabel = options.requestIdLogLabel genReqId = options.genReqId disableRequestLogging = options.disableRequestLogging @@ -397,7 +395,7 @@ function buildRouting (options) { req.headers[kRequestAcceptVersion] = undefined } - const id = req.headers[requestIdHeader] || genReqId(req) + const id = genReqId(req) const loggerBinding = { [requestIdLogLabel]: id diff --git a/test/logger.test.js b/test/logger.test.js index c084c2f2e5..48ba39a203 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -330,6 +330,51 @@ test('The request id header key can be customized', t => { }) }) +test('The request id header key can be ignored', t => { + t.plan(9) + const REQUEST_ID = 'ignore-me' + + const stream = split(JSON.parse) + const fastify = Fastify({ + logger: { stream, level: 'info' }, + requestIdHeader: false + }) + t.teardown(() => fastify.close()) + + fastify.get('/', (req, reply) => { + t.equal(req.id, 'req-1') + req.log.info('some log message') + reply.send({ id: req.id }) + }) + + fastify.inject({ + method: 'GET', + url: '/', + headers: { + 'request-id': REQUEST_ID + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.equal(payload.id, 'req-1') + + stream.once('data', line => { + t.equal(line.reqId, 'req-1') + t.equal(line.msg, 'incoming request', 'message is set') + + stream.once('data', line => { + t.equal(line.reqId, 'req-1') + t.equal(line.msg, 'some log message', 'message is set') + + stream.once('data', line => { + t.equal(line.reqId, 'req-1') + t.equal(line.msg, 'request completed', 'message is set') + }) + }) + }) + }) +}) + test('The request id header key can be customized along with a custom id generator', t => { t.plan(12) const REQUEST_ID = '42' @@ -393,6 +438,69 @@ test('The request id header key can be customized along with a custom id generat }) }) +test('The request id header key can be ignored along with a custom id generator', t => { + t.plan(12) + const REQUEST_ID = 'ignore-me' + + const stream = split(JSON.parse) + const fastify = Fastify({ + logger: { stream, level: 'info' }, + requestIdHeader: false, + genReqId (req) { + return 'foo' + } + }) + t.teardown(() => fastify.close()) + + fastify.get('/one', (req, reply) => { + t.equal(req.id, 'foo') + req.log.info('some log message') + reply.send({ id: req.id }) + }) + + fastify.get('/two', (req, reply) => { + t.equal(req.id, 'foo') + req.log.info('some log message 2') + reply.send({ id: req.id }) + }) + + const matches = [ + { reqId: 'foo', msg: /incoming request/ }, + { reqId: 'foo', msg: /some log message/ }, + { reqId: 'foo', msg: /request completed/ }, + { reqId: 'foo', msg: /incoming request/ }, + { reqId: 'foo', msg: /some log message 2/ }, + { reqId: 'foo', msg: /request completed/ } + ] + + let i = 0 + stream.on('data', line => { + t.match(line, matches[i]) + i += 1 + }) + + fastify.inject({ + method: 'GET', + url: '/one', + headers: { + 'request-id': REQUEST_ID + } + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.equal(payload.id, 'foo') + }) + + fastify.inject({ + method: 'GET', + url: '/two' + }, (err, res) => { + t.error(err) + const payload = JSON.parse(res.payload) + t.equal(payload.id, 'foo') + }) +}) + test('The request id log label can be changed', t => { t.plan(6) const REQUEST_ID = '42' diff --git a/test/types/fastify.test-d.ts b/test/types/fastify.test-d.ts index 66c4d64d17..515d5c3b93 100644 --- a/test/types/fastify.test-d.ts +++ b/test/types/fastify.test-d.ts @@ -121,6 +121,7 @@ expectAssignable(fastify({ serverFactory: () => http.createServer() })) expectAssignable(fastify({ caseSensitive: true })) expectAssignable(fastify({ requestIdHeader: 'request-id' })) +expectAssignable(fastify({ requestIdHeader: false })) expectAssignable(fastify({ genReqId: () => 'request-id' })) expectAssignable(fastify({ trustProxy: true })) expectAssignable(fastify({ querystringParser: () => ({ foo: 'bar' }) })) diff --git a/test/types/instance.test-d.ts b/test/types/instance.test-d.ts index a3dc17354d..eadd16443f 100644 --- a/test/types/instance.test-d.ts +++ b/test/types/instance.test-d.ts @@ -267,7 +267,7 @@ type InitialConfig = Readonly<{ onProtoPoisoning?: 'error' | 'remove' | 'ignore', onConstructorPoisoning?: 'error' | 'remove' | 'ignore', pluginTimeout?: number, - requestIdHeader?: string, + requestIdHeader?: string | false, requestIdLogLabel?: string, http2SessionTimeout?: number }> diff --git a/types/instance.d.ts b/types/instance.d.ts index a3fdef2afd..94f13e50f3 100644 --- a/types/instance.d.ts +++ b/types/instance.d.ts @@ -573,7 +573,7 @@ export interface FastifyInstance< onProtoPoisoning?: ProtoAction, onConstructorPoisoning?: ConstructorAction, pluginTimeout?: number, - requestIdHeader?: string, + requestIdHeader?: string | false, requestIdLogLabel?: string, http2SessionTimeout?: number }>