Last active
May 6, 2025 15:21
-
-
Save thom-nic/361cc4bb7e7d0d3ee7543b5f774b5286 to your computer and use it in GitHub Desktop.
Monkey-patch for ExpressJS v5 to allow `req.query` to be mutable by middleware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-invalid-this */ | |
const { request: Request } = require('express'); | |
const parseQuery = Object.getOwnPropertyDescriptor(Request, 'query').get; | |
function getQuery() { | |
if (this.__queryModified) | |
return this.__queryModified; | |
return parseQuery.call(this); | |
} | |
function setQuery(value) { | |
this.__queryModified = Object.freeze(value); | |
} | |
// Change the express.request class definition. This only needs to be called once and could have | |
// side effects if other code is using the request class and expecting `query` to not be mutable. | |
function monkeyPatchExpress5RequestQuery() { | |
Object.defineProperty(Request, 'query', { | |
configurable: true, | |
enumerable: true, | |
get: getQuery, | |
set: setQuery, | |
}); | |
}; | |
// Middleware solution provided by 'zecat' from https://stackoverflow.com/a/79604142/213983 | |
// This only patches the instance instead of modifying the class definition. It probably | |
// comes with a performance penalty since each instance is modified individually. | |
// This could be combined with the validation middleware, so that requests are only patched *if* | |
// query validation is performed. If there is a performance impact, it would be limited to | |
// routes where query validation is necessary, assuming not every route performs query validation. | |
function writableRequestQueryMiddleware(req, _res, next) { | |
Object.defineProperty( | |
req, | |
'query', | |
{ | |
...Object.getOwnPropertyDescriptor(req, 'query'), | |
value: req.query, | |
writable: true, | |
}); | |
next(); | |
} | |
module.exports = { | |
monkeyPatchExpressRequestQuery, | |
writableRequestQueryMiddleware, | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Trivial example ExpressJS v5 validator using Joi and Boom. | |
// There are many better ways to do this e.g. as middleware, supporting `body` and `params` etc. | |
function validateQuery(schema, req, next) { | |
try { | |
// this assumes we used either monkeyPatchExpress5RequestQuery() to patch the class, or writableRequestQueryMiddleware() earlier | |
// in the middleware pipeline to make req.query mutable | |
req.query = Joi.attempt(req.query, schema); | |
return true; // valid | |
} | |
catch (err) { | |
// `error` is a Joi ValidationError. You could extract `err.details` and | |
// put them in your Boom payload. | |
// See: https://joi.dev/api/?v=17.13.3#validationerror | |
// See: https://hapi.dev/module/boom/api/?v=10.0.1 | |
next(Boom.conflict('validation error', {data: err.details})); | |
return false; // invalid | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This combines the above two examples, and only modifies req.query when it must change due to validation | |
// req.query remains immutable after changing it here. | |
// update req.query while keeping the property immutable afterwards | |
function updateQuery(req, value) { | |
Object.defineProperty( | |
req, | |
'query', | |
{ | |
...Object.getOwnPropertyDescriptor(req, 'query'), | |
writable: false, | |
value, | |
}); | |
} | |
// this validator does not require any other middleware or monkey-patching. | |
// req.query will be updated to the value returned by Joi.attempt() but req.query will | |
// still be immutable after. | |
function validateQuery(schema, req, next) { | |
try { | |
updateQuery(req, Joi.attempt(req.query, schema)); | |
return true; // valid | |
} | |
catch (err) { | |
// `error` is a Joi ValidationError. You could extract `err.details` and | |
// put them in your Boom payload. | |
// See: https://joi.dev/api/?v=17.13.3#validationerror | |
// See: https://hapi.dev/module/boom/api/?v=10.0.1 | |
next(Boom.conflict('validation error', {data: err.details})); | |
return false; // invalid | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment