From 3490efd9870a54b7d55e47eada4afbecd2961f64 Mon Sep 17 00:00:00 2001 From: Jon Church Date: Thu, 27 Aug 2026 16:21:10 -0400 Subject: [PATCH 1/2] feat: NO_COLOR env var removes color in dev format --- README.md | 5 ++ index.js | 13 +++ package.json | 2 +- test/morgan.js | 3 + test/noColor.js | 205 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 test/noColor.js diff --git a/README.md b/README.md index 5c61bc78..e5a6800a 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,11 @@ for information codes. GET /dev 200 0.224 ms - 2 ``` +Coloring is disabled when the [`NO_COLOR`](https://no-color.org) environment +variable is set to any non-empty value, in which case the output above is +written without any escape sequences. The variable is read once, when `morgan` +is first required. + ##### short Shorter than default, also including response time. diff --git a/index.js b/index.js index fc71056f..a5b78e43 100644 --- a/index.js +++ b/index.js @@ -47,6 +47,8 @@ var CLF_MONTH = [ var DEFAULT_BUFFER_DURATION = 1000 +var NO_COLOR = Boolean(process.env.NO_COLOR) + /** * Escape control characters and backslashes so a value is safe for * line-oriented logs. @@ -211,6 +213,17 @@ morgan.format('tiny', ':method :url :status :res[content-length] - :response-tim */ morgan.format('dev', function developmentFormatLine (tokens, req, res) { + // when NO_COLOR is set, use an escape free variant of dev format + if (NO_COLOR) { + var noColorFn = developmentFormatLine.noColor + + if (!noColorFn) { + noColorFn = developmentFormatLine.noColor = compile(':method :url :status :response-time ms - :res[content-length]') + } + + return noColorFn(tokens, req, res) + } + // get the status code if response written var status = headersSent(res) ? res.statusCode diff --git a/package.json b/package.json index 1c312127..236edf09 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ }, "scripts": { "lint": "eslint --plugin markdown --ext js,md .", - "test": "mocha --check-leaks --reporter spec", + "test": "mocha --check-leaks --reporter spec test/morgan.js && mocha --check-leaks --reporter spec test/noColor.js", "test-ci": "nyc --reporter=lcov --reporter=text npm test", "test-cov": "nyc --reporter=html --reporter=text npm test" } diff --git a/test/morgan.js b/test/morgan.js index 08e4d16b..baa5d40e 100644 --- a/test/morgan.js +++ b/test/morgan.js @@ -1,5 +1,8 @@ process.env.NO_DEPRECATION = 'morgan' +// empty, not deleted: per https://no-color.org an empty NO_COLOR must not +// disable color, so the dev tests below guard that. Set: test/noColor.js +process.env.NO_COLOR = '' var assert = require('assert') var fs = require('fs') diff --git a/test/noColor.js b/test/noColor.js new file mode 100644 index 00000000..c4d8930e --- /dev/null +++ b/test/noColor.js @@ -0,0 +1,205 @@ + +process.env.NO_DEPRECATION = 'morgan' +// read once at require time, hence a separate file from test/morgan.js +process.env.NO_COLOR = '1' + +var assert = require('assert') +var http = require('http') +var morgan = require('..') +var request = require('supertest') +var split = require('split') + +describe('morgan()', function () { + describe('formats', function () { + describe('dev', function () { + it('should not color 1xx', function (done) { + var cb = after(2, function (err, res, line) { + if (err) return done(err) + assertPlainDevLine(line, 102) + done() + }) + + var stream = createLineStream(function onLine (line) { + cb(null, null, line) + }) + + var server = createServer('dev', { stream: stream }, function (req, res, next) { + res.statusCode = 102 + next() + }) + + request(server) + .get('/') + .expect(102, function (err, res) { + if (err && err.code === 'ECONNRESET') { + // finishing response with 1xx is invalid http + // but node.js server lets the server do this, so + // morgan needs to test in this condition even if + // the http client doesn't like it + err = null + } + cb(err, res) + }) + }) + + it('should not color 2xx', function (done) { + var cb = after(2, function (err, res, line) { + if (err) return done(err) + assertPlainDevLine(line, 200) + done() + }) + + var stream = createLineStream(function onLine (line) { + cb(null, null, line) + }) + + var server = createServer('dev', { stream: stream }, function (req, res, next) { + res.statusCode = 200 + next() + }) + + request(server) + .get('/') + .expect(200, cb) + }) + + it('should not color 3xx', function (done) { + var cb = after(2, function (err, res, line) { + if (err) return done(err) + assertPlainDevLine(line, 300) + done() + }) + + var stream = createLineStream(function onLine (line) { + cb(null, null, line) + }) + + var server = createServer('dev', { stream: stream }, function (req, res, next) { + res.statusCode = 300 + next() + }) + + request(server) + .get('/') + .expect(300, cb) + }) + + it('should not color 4xx', function (done) { + var cb = after(2, function (err, res, line) { + if (err) return done(err) + assertPlainDevLine(line, 400) + done() + }) + + var stream = createLineStream(function onLine (line) { + cb(null, null, line) + }) + + var server = createServer('dev', { stream: stream }, function (req, res, next) { + res.statusCode = 400 + next() + }) + + request(server) + .get('/') + .expect(400, cb) + }) + + it('should not color 5xx', function (done) { + var cb = after(2, function (err, res, line) { + if (err) return done(err) + assertPlainDevLine(line, 500) + done() + }) + + var stream = createLineStream(function onLine (line) { + cb(null, null, line) + }) + + var server = createServer('dev', { stream: stream }, function (req, res, next) { + res.statusCode = 500 + next() + }) + + request(server) + .get('/') + .expect(500, cb) + }) + + it('should match the documented dev format', function (done) { + var cb = after(2, function (err, res, line) { + if (err) return done(err) + assert.ok(/^GET \/ 200 \d+\.\d{3} ms - -$/.test(line), + 'unexpected line ' + JSON.stringify(line)) + done() + }) + + var stream = createLineStream(function onLine (line) { + cb(null, null, line) + }) + + request(createServer('dev', { stream: stream })) + .get('/') + .expect(200, cb) + }) + }) + }) +}) + +function after (count, callback) { + var args = new Array(3) + var i = 0 + + return function (err, arg1, arg2) { + assert.ok(i++ < count, 'callback called ' + count + ' times') + + args[0] = args[0] || err + args[1] = args[1] || arg1 + args[2] = args[2] || arg2 + + if (count === i) { + callback.apply(null, args) + } + } +} + +function assertPlainDevLine (line, status) { + assert.strictEqual(line.indexOf('\x1b'), -1, 'expected no escapes in ' + JSON.stringify(line)) + assert.ok( + new RegExp('^GET / ' + status + ' \\d+\\.\\d{3} ms - (\\d+|-)$').test(line), + 'unexpected line ' + JSON.stringify(line) + ) +} + +function createLineStream (callback) { + return split().on('data', callback) +} + +function createServer (format, opts, fn, fn1) { + var logger = morgan(format, opts) + var middle = fn || noopMiddleware + + return http.createServer().on('request', function onRequest (req, res) { + // prior alterations + if (fn1) { + fn1(req, res) + } + + logger(req, res, function onNext (err) { + // allow req, res alterations + middle(req, res, function onDone () { + if (err) { + res.statusCode = 500 + res.end(err.message) + } + + res.setHeader('X-Sent', 'true') + res.end((req.connection && req.connection.remoteAddress) || '-') + }) + }) + }) +} + +function noopMiddleware (req, res, next) { + next() +} From b10cd56017c03549f61cbe6b89581853e7f08428 Mon Sep 17 00:00:00 2001 From: Jon Church Date: Thu, 27 Aug 2026 16:50:30 -0400 Subject: [PATCH 2/2] refactor: replace per-request NO_COLOR check with load-time dev format registration --- index.js | 17 ++++++----------- test/noColor.js | 5 +++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index a5b78e43..449cde5c 100644 --- a/index.js +++ b/index.js @@ -213,17 +213,6 @@ morgan.format('tiny', ':method :url :status :res[content-length] - :response-tim */ morgan.format('dev', function developmentFormatLine (tokens, req, res) { - // when NO_COLOR is set, use an escape free variant of dev format - if (NO_COLOR) { - var noColorFn = developmentFormatLine.noColor - - if (!noColorFn) { - noColorFn = developmentFormatLine.noColor = compile(':method :url :status :response-time ms - :res[content-length]') - } - - return noColorFn(tokens, req, res) - } - // get the status code if response written var status = headersSent(res) ? res.statusCode @@ -248,6 +237,12 @@ morgan.format('dev', function developmentFormatLine (tokens, req, res) { return fn(tokens, req, res) }) +// NO_COLOR (https://no-color.org): when set and not an empty string, the dev +// format is replaced at load time with a variant free of escape sequences +if (NO_COLOR) { + morgan.format('dev', compile(':method :url :status :response-time ms - :res[content-length]')) +} + /** * request url */ diff --git a/test/noColor.js b/test/noColor.js index c4d8930e..ec86bfd9 100644 --- a/test/noColor.js +++ b/test/noColor.js @@ -1,7 +1,8 @@ process.env.NO_DEPRECATION = 'morgan' -// read once at require time, hence a separate file from test/morgan.js -process.env.NO_COLOR = '1' +// read once at require time, hence a separate file from test/morgan.js. +// '0' is deliberate: any non-empty value disables color, whatever it is +process.env.NO_COLOR = '0' var assert = require('assert') var http = require('http')