Compare commits

..

15 Commits
5.x ... master

Author SHA1 Message Date
Ishita Singh
b4ab7d65d7 test: include edge case tests for res.type() (#7037) 2026-02-23 10:58:26 +01:00
Pavan Shinde
c4cc78bdf5 docs: fix README security policy link (#7029) 2026-02-21 22:15:11 -05:00
Dave Tashner
925a1dff1e fix: bump qs minimum to ^6.14.2 for CVE-2026-2391 (#7057)
qs versions before 6.14.2 have an arrayLimit bypass in comma parsing
that allows denial of service (GHSA-w7fw-mjwx-w883).

While the existing ^6.14.1 semver range allows 6.14.2 on fresh
installs, bumping the minimum ensures the vulnerable version cannot
be resolved.

Signed-off-by: davetashner <5702882+davetashner@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 22:11:08 -05:00
Murat Kirazkaya
9c85a25c02 Remove duplicate tests in res.location and res.jsonp (#6996)
* test: remove duplicated tests

* test: fix typo in data URI encoding test description
2026-02-14 12:25:36 -05:00
dependabot[bot]
1140301f6a build(deps): bump github/codeql-action from 4.31.9 to 4.32.0 (#7013)
* build(deps): bump github/codeql-action from 4.31.9 to 4.32.0

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.9 to 4.32.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](5d4e8d1aca...b20883b0cd)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: fix version tag comments

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Phillip Barta <barta.phillip@gmail.com>
2026-02-10 00:10:13 +01:00
dependabot[bot]
c76ed5ae05 build(deps): bump actions/setup-node from 6.1.0 to 6.2.0 (#7012)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](395ad32622...6044e13b5d)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 00:05:17 +01:00
dependabot[bot]
2d4192ebb3 build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (#7011)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](8e8c483db8...de0fac2e45)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 00:04:00 +01:00
Sebastian Beltran
66404b347a docs: Add @GroophyLifefor to the triage team (#6995) 2026-02-01 13:04:08 +01:00
Viny Brun Kriesang
d12772393c fix: search example to support Redis v4+ and Express 4/5 (#6274)
* Fix Redis example to support Redis v4+ and Express 4/5

* update optional route syntax to /{:query} and refactor Redis initialization into dedicated function to guarantee that it is complete before server starts

---------

Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com>
2026-01-31 22:12:23 -05:00
Ayoub Mabrouk
6b7ccfcf12 test: add test for normalizeType fallback when mime lookup fails (#6894)
Add test to verify that utils.normalizeType correctly defaults to
'application/octet-stream' when mime.lookup() returns null/undefined
for unknown file extensions. This covers the fallback behavior on
line 64 of lib/utils.js and ensures proper handling of unrecognized
MIME types.

Co-authored-by: bjohansebas <103585995+bjohansebas@users.noreply.github.com>
2026-01-31 21:53:38 -05:00
AkaHarshit
c9ecf7b658 feat: Allow passing null or undefined as the value for options in app.render (#6903)
* fix: allow null options in app.render

* fix: ensure options are initialized to an empty object in app.render

* docs: add history entry

---------

Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com>
2026-01-31 21:51:17 -05:00
Sebastian Beltran
a479419b16 feat: do not modify the Content-Type twice when sending strings (#6991)
* fix: improve content-type handling in res.send method

* fix: ensure content-type is a string before setting charset in res.send

* fix: refactor content-type handling in res.send to use const and improve clarity

* Apply suggestion from @bjohansebas

* docs: update History.md
2026-01-19 09:56:53 -05:00
Sebastian Beltran
5a4568abfe chore: remove benchmarks directory (#6992) 2026-01-17 17:36:22 -05:00
sukdev24
912893c07c test: added unit tests for utils.compileETag to cover valid and invalid inputs (#6534)
* Added unit tests for utils.compileETag to cover valid and invalid inputs

* test: enhance compileETag tests for various input types

---------

Co-authored-by: sucem029 <sucem029@vippan-118.ad.liu.se>
Co-authored-by: Sebastian Beltran <bjohansebas@gmail.com>
2026-01-16 21:27:22 -05:00
Marcos Molina
ae265a90c7 docs: fix JSDoc for req.accepts() return value and parameter format (#6936)
* fixed request accept jsdoc

* reverted format

* reverted format

* updated jsdoc

* updated the rest of the documentation
2026-01-16 16:19:39 -05:00
20 changed files with 201 additions and 175 deletions

View File

@@ -27,11 +27,11 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
@@ -53,12 +53,12 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
@@ -93,7 +93,7 @@ jobs:
contents: read
checks: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

View File

@@ -39,13 +39,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
config: |
@@ -71,4 +71,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0

View File

@@ -37,12 +37,12 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node-version }}
@@ -77,7 +77,7 @@ jobs:
contents: read
checks: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -67,6 +67,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5
uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
sarif_file: results.sarif

View File

@@ -2,7 +2,17 @@
## 🚀 Improvements
- Improve HTML structure in `res.redirect()` responses when HTML format is accepted by adding `<!DOCTYPE html>`, `<title>`, and `<body>` tags for better browser compatibility - by [@Bernice55231](https://github.com/Bernice55231) in [#5167](https://github.com/expressjs/express/pull/5167)
* Improve HTML structure in `res.redirect()` responses when HTML format is accepted by adding `<!DOCTYPE html>`, `<title>`, and `<body>` tags for better browser compatibility - by [@Bernice55231](https://github.com/Bernice55231) in [#5167](https://github.com/expressjs/express/pull/5167)
* When calling `app.render` with options set to null, the locals object is handled correctly, preventing unexpected errors and making the method behave the same as when options is omitted or an empty object is passed - by [AkaHarshit](https://github.com/AkaHarshit) in [#6903](https://github.com/expressjs/express/pull/6903)
```js
app.render('index', null, callback); // now works as expected
```
## ⚡ Performance
* Avoid duplicate Content-Type header processing in `res.send()` when sending string responses without an explicit Content-Type header - by [@bjohansebas](https://github.com/bjohansebas) in [#6991](https://github.com/expressjs/express/pull/6991)
5.2.1 / 2025-12-01
=======================

View File

@@ -154,7 +154,7 @@ See the [Contributing Guide] for more technical details on contributing.
### Security Issues
If you discover a security vulnerability in Express, please see [Security Policies and Procedures](https://github.com/expressjs/express?tab=security-ov-file).
If you discover a security vulnerability in Express, please see [Security Policies and Procedures](https://github.com/expressjs/express/security/policy).
### Running Tests
@@ -214,6 +214,7 @@ The original author of Express is [TJ Holowaychuk](https://github.com/tj)
* [efekrskl](https://github.com/efekrskl) - **Efe Karasakal**
* [rxmarbles](https://github.com/rxmarbles) - **Rick Markins** (he/him)
* [krzysdz](https://github.com/krzysdz)
* [GroophyLifefor](https://github.com/GroophyLifefor) - **Murat Kirazkaya**
<details>
<summary>Triagers emeriti members</summary>

View File

@@ -1,17 +0,0 @@
all:
@./run 1 middleware 50
@./run 5 middleware 50
@./run 10 middleware 50
@./run 15 middleware 50
@./run 20 middleware 50
@./run 30 middleware 50
@./run 50 middleware 50
@./run 100 middleware 50
@./run 10 middleware 100
@./run 10 middleware 250
@./run 10 middleware 500
@./run 10 middleware 1000
@echo
.PHONY: all

View File

@@ -1,34 +0,0 @@
# Express Benchmarks
## Installation
You will need to install [wrk](https://github.com/wg/wrk/blob/master/INSTALL) in order to run the benchmarks.
## Running
To run the benchmarks, first install the dependencies `npm i`, then run `make`
The output will look something like this:
```
50 connections
1 middleware
7.15ms
6784.01
[...redacted...]
1000 connections
10 middleware
139.21ms
6155.19
```
### Tip: Include Node.js version in output
You can use `make && node -v` to include the node.js version in the output.
### Tip: Save the results to a file
You can use `make > results.log` to save the results to a file `results.log`.

View File

@@ -1,20 +0,0 @@
var express = require('..');
var app = express();
// number of middleware
var n = parseInt(process.env.MW || '1', 10);
console.log(' %s middleware', n);
while (n--) {
app.use(function(req, res, next){
next();
});
}
app.use(function(req, res){
res.send('Hello World')
});
app.listen(3333);

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
echo
MW=$1 node $2 &
pid=$!
echo " $3 connections"
sleep 2
wrk 'http://localhost:3333/?foo[bar]=baz' \
-d 3 \
-c $3 \
-t 8 \
| grep 'Requests/sec\|Latency' \
| awk '{ print " " $2 }'
kill $pid

View File

@@ -16,31 +16,47 @@ var path = require('node:path');
var redis = require('redis');
var db = redis.createClient();
// npm install redis
var app = express();
app.use(express.static(path.join(__dirname, 'public')));
// populate search
// npm install redis
db.sadd('ferret', 'tobi');
db.sadd('ferret', 'loki');
db.sadd('ferret', 'jane');
db.sadd('cat', 'manny');
db.sadd('cat', 'luna');
/**
* Redis Initialization
*/
async function initializeRedis() {
try {
// connect to Redis
await db.connect();
// populate search
await db.sAdd('ferret', 'tobi');
await db.sAdd('ferret', 'loki');
await db.sAdd('ferret', 'jane');
await db.sAdd('cat', 'manny');
await db.sAdd('cat', 'luna');
} catch (err) {
console.error('Error initializing Redis:', err);
process.exit(1);
}
}
/**
* GET search for :query.
*/
app.get('/search/:query?', function(req, res, next){
var query = req.params.query;
db.smembers(query, function(err, vals){
if (err) return next(err);
res.send(vals);
});
app.get('/search/{:query}', function (req, res, next) {
var query = req.params.query || '';
db.sMembers(query)
.then((vals) => res.send(vals))
.catch((err) => {
console.error(`Redis error for query "${query}":`, err);
next(err);
});
});
/**
@@ -54,8 +70,14 @@ app.get('/client.js', function(req, res){
res.sendFile(path.join(__dirname, 'client.js'));
});
/* istanbul ignore next */
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
/**
* Start the Server
*/
(async () => {
await initializeRedis();
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
})();

View File

@@ -523,7 +523,7 @@ app.render = function render(name, options, callback) {
var cache = this.cache;
var done = callback;
var engines = this.engines;
var opts = options;
var opts = options || {};
var view;
// support callback function as second arg

View File

@@ -83,16 +83,13 @@ req.header = function header(name) {
};
/**
* To do: update docs.
*
* Check if the given `type(s)` is acceptable, returning
* the best match when true, otherwise `undefined`, in which
* the best match when true, otherwise `false`, in which
* case you should respond with 406 "Not Acceptable".
*
* The `type` value may be a single MIME type string
* such as "application/json", an extension name
* such as "json", a comma-delimited list such as "json, html, text/plain",
* an argument list such as `"json", "html", "text/plain"`,
* such as "json", an argument list such as `"json", "html", "text/plain"`,
* or an array `["json", "html", "text/plain"]`. When a list
* or array is given, the _best_ match, if any is returned.
*
@@ -107,7 +104,7 @@ req.header = function header(name) {
* // => "html"
* req.accepts('text/html');
* // => "text/html"
* req.accepts('json, text');
* req.accepts('json', 'text');
* // => "json"
* req.accepts('application/json');
* // => "application/json"
@@ -115,12 +112,11 @@ req.header = function header(name) {
* // Accept: text/*, application/json
* req.accepts('image/png');
* req.accepts('png');
* // => undefined
* // => false
*
* // Accept: text/*;q=.5, application/json
* req.accepts(['html', 'json']);
* req.accepts('html', 'json');
* req.accepts('html, json');
* // => "json"
*
* @param {String|Array} type(s)

View File

@@ -126,7 +126,6 @@ res.send = function send(body) {
var chunk = body;
var encoding;
var req = this.req;
var type;
// settings
var app = this.app;
@@ -134,7 +133,12 @@ res.send = function send(body) {
switch (typeof chunk) {
// string defaulting to html
case 'string':
if (!this.get('Content-Type')) {
encoding = 'utf8';
const type = this.get('Content-Type');
if (typeof type === 'string') {
this.set('Content-Type', setCharset(type, 'utf-8'));
} else {
this.type('html');
}
break;
@@ -153,17 +157,6 @@ res.send = function send(body) {
break;
}
// write strings in utf-8
if (typeof chunk === 'string') {
encoding = 'utf8';
type = this.get('Content-Type');
// reflect this in content-type
if (typeof type === 'string') {
this.set('Content-Type', setCharset(type, 'utf-8'));
}
}
// determine if ETag should be generated
var etagFn = app.get('etag fn')
var generateETag = !this.get('ETag') && typeof etagFn === 'function'

View File

@@ -52,7 +52,7 @@
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.1",
"qs": "^6.14.2",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",

View File

@@ -331,6 +331,24 @@ describe('app', function(){
})
})
it('should accept null or undefined options', function (done) {
var app = createApp()
app.set('views', path.join(__dirname, 'fixtures'))
app.locals.user = { name: 'tobi' }
app.render('user.tmpl', null, function (err, str) {
if (err) return done(err);
assert.strictEqual(str, '<p>tobi</p>')
app.render('user.tmpl', undefined, function (err2, str2) {
if (err2) return done(err2);
assert.strictEqual(str2, '<p>tobi</p>')
done()
})
})
})
describe('caching', function(){
it('should cache with cache option', function(done){
var app = express();

View File

@@ -327,18 +327,4 @@ describe('res', function(){
})
})
})
it('should not override previous Content-Types', function(done){
var app = express();
app.get('/', function(req, res){
res.type('application/vnd.example+json');
res.jsonp({ hello: 'world' });
});
request(app)
.get('/')
.expect('content-type', 'application/vnd.example+json; charset=utf-8')
.expect(200, '{"hello":"world"}', done)
})
})

View File

@@ -46,19 +46,7 @@ describe('res', function(){
.expect(200, done)
})
it('should encode data uri1', function (done) {
var app = express()
app.use(function (req, res) {
res.location('data:text/javascript,export default () => { }').end();
});
request(app)
.get('/')
.expect('Location', 'data:text/javascript,export%20default%20()%20=%3E%20%7B%20%7D')
.expect(200, done)
})
it('should encode data uri2', function (done) {
it('should encode data uri', function (done) {
var app = express()
app.use(function (req, res) {
res.location('data:text/javascript,export default () => { }').end();

View File

@@ -42,5 +42,74 @@ describe('res', function(){
.get('/')
.expect('Content-Type', 'application/vnd.amazon.ebook', done);
})
describe('edge cases', function(){
it('should handle empty string gracefully', function(done){
var app = express();
app.use(function(req, res){
res.type('').end('test');
});
request(app)
.get('/')
.expect('Content-Type', 'application/octet-stream')
.end(done);
})
it('should handle file extension with dots', function(done){
var app = express();
app.use(function(req, res){
res.type('.json').end('{"test": true}');
});
request(app)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8')
.end(done);
})
it('should handle multiple file extensions', function(done){
var app = express();
app.use(function(req, res){
res.type('file.tar.gz').end('compressed');
});
request(app)
.get('/')
.expect('Content-Type', 'application/gzip')
.end(done);
})
it('should handle uppercase extensions', function(done){
var app = express();
app.use(function(req, res){
res.type('FILE.JSON').end('{"test": true}');
});
request(app)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8')
.end(done);
})
it('should handle extension with special characters', function(done){
var app = express();
app.use(function(req, res){
res.type('file@test.json').end('{"test": true}');
});
request(app)
.get('/')
.expect('Content-Type', 'application/json; charset=utf-8')
.end(done);
})
})
})
})

View File

@@ -35,8 +35,15 @@ describe('utils.normalizeType acceptParams method', () => {
params: {} // No parameters are added since "invalid" has no "="
});
});
});
it('should default to application/octet-stream when mime lookup fails', () => {
const result = utils.normalizeType('unknown-extension-xyz');
assert.deepEqual(result, {
value: 'application/octet-stream',
params: {}
});
});
});
describe('utils.setCharset(type, charset)', function () {
it('should do anything without type', function () {
@@ -81,3 +88,28 @@ describe('utils.wetag(body, encoding)', function(){
'W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"')
})
})
describe('utils.compileETag()', function () {
it('should return generateETag for true', function () {
const fn = utils.compileETag(true);
assert.strictEqual(fn('express!'), utils.wetag('express!'));
});
it('should return undefined for false', function () {
assert.strictEqual(utils.compileETag(false), undefined);
});
it('should return generateETag for string values "strong" and "weak"', function () {
assert.strictEqual(utils.compileETag('strong')("express"), utils.etag("express"));
assert.strictEqual(utils.compileETag('weak')("express"), utils.wetag("express"));
});
it('should throw for unknown string values', function () {
assert.throws(() => utils.compileETag('foo'), TypeError);
});
it('should throw for unsupported types like arrays and objects', function () {
assert.throws(() => utils.compileETag([]), TypeError);
assert.throws(() => utils.compileETag({}), TypeError);
});
});