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
}>