Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ benchmarks/graphs

# ignore additional files using core.excludesFile
# https://git-scm.com/docs/gitignore

AGENTS.md
CLAUDE.md
IMPLEMENTATION_SUMMARY.md
9 changes: 8 additions & 1 deletion lib/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,14 @@ app.set = function set(setting, val) {
this.set('etag fn', compileETag(val));
break;
case 'query parser':
this.set('query parser fn', compileQueryParser(val));
this.set('query parser fn', compileQueryParser(val, this.get('query parser options')));
break;
case 'query parser options':
// Re-compile the query parser with new options
var currentParser = this.get('query parser');
if (currentParser) {
this.set('query parser fn', compileQueryParser(currentParser, val));
}
break;
case 'trust proxy':
this.set('trust proxy fn', compileTrust(val));
Expand Down
4 changes: 3 additions & 1 deletion lib/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@ res.send = function send(body) {
// populate ETag
var etag;
if (generateETag && len !== undefined) {
if ((etag = etagFn(chunk, encoding))) {
// Pass response headers to ETag function for CORS-aware ETags
var responseHeaders = this.getHeaders ? this.getHeaders() : this._headers || {};
if ((etag = etagFn(chunk, encoding, responseHeaders))) {
this.set('ETag', etag);
}
}
Expand Down
97 changes: 84 additions & 13 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ exports.etag = createETagGenerator({ weak: false })

exports.wetag = createETagGenerator({ weak: true })

/**
* Return strong ETag for `body` including CORS headers.
*
* @param {String|Buffer} body
* @param {String} [encoding]
* @param {Object} [headers]
* @return {String}
* @api private
*/

exports.etagCors = createETagGenerator({
weak: false,
includeHeaders: ['access-control-allow-origin']
})

/**
* Return weak ETag for `body` including CORS headers.
*
* @param {String|Buffer} body
* @param {String} [encoding]
* @param {Object} [headers]
* @return {String}
* @api private
*/

exports.wetagCors = createETagGenerator({
weak: true,
includeHeaders: ['access-control-allow-origin']
})

/**
* Normalize the given `type`, for example "html" becomes "text/html".
*
Expand Down Expand Up @@ -144,6 +174,12 @@ exports.compileETag = function(val) {
case 'strong':
fn = exports.etag;
break;
case 'weak-cors':
fn = exports.wetagCors;
break;
case 'strong-cors':
fn = exports.etagCors;
break;
default:
throw new TypeError('unknown value for etag function: ' + val);
}
Expand All @@ -155,11 +191,12 @@ exports.compileETag = function(val) {
* Compile "query parser" value to function.
*
* @param {String|Function} val
* @param {Object} [qsOptions] - Options for qs parser
* @return {Function}
* @api private
*/

exports.compileQueryParser = function compileQueryParser(val) {
exports.compileQueryParser = function compileQueryParser(val, qsOptions) {
var fn;

if (typeof val === 'function') {
Expand All @@ -174,7 +211,7 @@ exports.compileQueryParser = function compileQueryParser(val) {
case false:
break;
case 'extended':
fn = parseExtendedQueryString;
fn = createExtendedQueryParser(qsOptions);
break;
default:
throw new TypeError('unknown value for query parser function: ' + val);
Expand Down Expand Up @@ -242,30 +279,64 @@ exports.setCharset = function setCharset(type, charset) {
* the given options.
*
* @param {object} options
* @param {boolean} options.weak - Generate weak ETags
* @param {string[]} options.includeHeaders - Response headers to include in hash
* @return {function}
* @private
*/

function createETagGenerator (options) {
return function generateETag (body, encoding) {
var weak = options.weak;
var includeHeaders = options.includeHeaders || [];

return function generateETag (body, encoding, headers) {
var buf = !Buffer.isBuffer(body)
? Buffer.from(body, encoding)
: body
: body;

return etag(buf, options)
}
// If no headers to include, use body-only hashing (backward compatible)
if (includeHeaders.length === 0 || !headers) {
return etag(buf, { weak: weak });
}

// Combine body with specified headers
var headerParts = includeHeaders
.map(function(name) {
var value = headers[name.toLowerCase()];
return value ? String(value) : '';
})
.filter(Boolean);

if (headerParts.length === 0) {
// No headers present, fall back to body-only
return etag(buf, { weak: weak });
}

// Create combined buffer: body + header values
var headerBuf = Buffer.from(headerParts.join('|'), 'utf8');
var combined = Buffer.concat([buf, Buffer.from('|'), headerBuf]);

return etag(combined, { weak: weak });
};
}

/**
* Parse an extended query string with qs.
* Create an extended query string parser with qs.
*
* @param {String} str
* @return {Object}
* @param {Object} [options] - Options for qs.parse
* @return {Function}
* @private
*/

function parseExtendedQueryString(str) {
return qs.parse(str, {
allowPrototypes: true
});
function createExtendedQueryParser(options) {
var qsOptions = Object.assign({
allowPrototypes: true, // Backward compatibility (consider changing to false in v6)
parameterLimit: 1000, // Explicit default
arrayLimit: 20, // qs default
depth: 5 // qs default
}, options || {});

return function parseExtendedQueryString(str) {
return qs.parse(str, qsOptions);
};
}
Loading