From 95e23ba94bb8be508b92c73555ec15ea59fea528 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 14 Apr 2026 15:16:48 -0400 Subject: [PATCH 01/13] feat: add TracingChannel support for express:request Add a single `express:request` TracingChannel that emits structured lifecycle events (start, end, asyncStart, asyncEnd, error) for every middleware, route handler, and error handler execution. Context shape: { req, res, layer } where layer is the Layer instance, giving subscribers access to layer.name, layer.handle.length (for error handler detection), and layer.route. Consumers decide how to classify and name spans based on these properties. Route dispatch wrapper layers (internal glue that calls route.dispatch) are excluded from tracing to avoid duplicate events. The actual user handlers inside the route are traced individually. Zero overhead when no subscribers are registered. The hasSubscribers check gates all context allocation and tracePromise wrapping. Refs: https://github.com/pillarjs/router/pull/96 Refs: https://github.com/expressjs/express/issues/6353 --- lib/layer.js | 86 ++++++++--- test/tracing.js | 402 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 468 insertions(+), 20 deletions(-) create mode 100644 test/tracing.js diff --git a/lib/layer.js b/lib/layer.js index 6a4408f..98d2ad2 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -12,6 +12,7 @@ * @private */ +const dc = require('diagnostics_channel') const isPromise = require('is-promise') const pathRegexp = require('path-to-regexp') const debug = require('debug')('router:layer') @@ -25,6 +26,20 @@ const deprecate = require('depd')('router') const TRAILING_SLASH_REGEXP = /\/+$/ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g +/** + * TracingChannel setup. + * @private + */ + +const requestChannel = dc.tracingChannel('express:request') + +/** + * Check if the channel has subscribers. + */ +function shouldTrace (ch) { + return ch && ch.start.hasSubscribers !== false +} + /** * Expose `Layer`. */ @@ -111,23 +126,12 @@ Layer.prototype.handleError = function handleError (error, req, res, next) { return next(error) } - try { - // invoke function - const ret = fn(error, req, res, next) - - // wait for returned promise - if (isPromise(ret)) { - if (!(ret instanceof Promise)) { - deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') - } - - ret.then(null, function (error) { - next(error || new Error('Rejected promise')) - }) - } - } catch (err) { - next(err) - } + const layer = this + invokeWithTrace(function () { + return fn(error, req, res, next) + }, function () { + return { req, res, layer } + }, next) } /** @@ -147,11 +151,53 @@ Layer.prototype.handleRequest = function handleRequest (req, res, next) { return next() } + // Skip tracing for route dispatch wrappers (this.route is only set on + // the internal layer that calls route.dispatch). The actual user handlers + // inside the route are traced individually. + if (this.route) { + return invokeWithTrace(function () { + return fn(req, res, next) + }, null, next) + } + + const layer = this + invokeWithTrace(function () { + return fn(req, res, next) + }, function () { + return { req, res, layer } + }, next) +} + +/** + * Invoke a handler function, optionally wrapping it in TracingChannel. + * The ctxFactory is only called when tracing is active, ensuring zero + * allocation overhead when no subscribers are registered. + * @private + */ + +function invokeWithTrace (exec, ctxFactory, next) { + if (ctxFactory && shouldTrace(requestChannel)) { + try { + requestChannel.tracePromise(function () { + const ret = exec() + if (isPromise(ret)) { + if (!(ret instanceof Promise)) { + deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') + } + return ret + } + }, ctxFactory()).then(null, function (error) { + next(error || new Error('Rejected promise')) + }) + } catch (err) { + next(err) + } + return + } + try { - // invoke function - const ret = fn(req, res, next) + const ret = exec() - // wait for returned promise if (isPromise(ret)) { if (!(ret instanceof Promise)) { deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') diff --git a/test/tracing.js b/test/tracing.js new file mode 100644 index 0000000..c817d04 --- /dev/null +++ b/test/tracing.js @@ -0,0 +1,402 @@ +const { it, describe, beforeEach, afterEach } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') + +const assert = utils.assert +const createServer = utils.createServer +const request = utils.request + +let dc +let tracingChannel + +try { + dc = require('node:diagnostics_channel') + if (dc.tracingChannel) { + tracingChannel = dc.tracingChannel + } +} catch {} + +const describeTracing = tracingChannel ? describe : describe.skip + +describeTracing('TracingChannel', function () { + let handlers + let events + + beforeEach(function () { + events = [] + handlers = { + start (ctx) { events.push({ phase: 'start', ctx }) }, + end (ctx) { events.push({ phase: 'end', ctx }) }, + asyncStart (ctx) { events.push({ phase: 'asyncStart', ctx }) }, + asyncEnd (ctx) { events.push({ phase: 'asyncEnd', ctx }) }, + error (ctx) { events.push({ phase: 'error', ctx }) } + } + }) + + afterEach(function () { + dc.tracingChannel('express:request').unsubscribe(handlers) + }) + + describe('when no subscribers', function () { + it('should not affect normal behavior', function (done) { + const router = new Router() + const server = createServer(router) + + router.get('/foo', function (req, res) { + res.statusCode = 200 + res.end('hello') + }) + + request(server) + .get('/foo') + .expect(200, 'hello', done) + }) + }) + + describe('context shape', function () { + it('should provide req, res, and layer in context', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.use(function myMiddleware (req, res, next) { + next() + }) + + router.get('/foo', function (req, res) { + res.statusCode = 200 + res.end('hello') + }) + + request(server) + .get('/foo') + .expect(200, function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const middlewareStart = startEvents.find(function (e) { + return e.ctx.layer && e.ctx.layer.name === 'myMiddleware' + }) + + assert.ok(middlewareStart, 'should have start event for myMiddleware') + assert.ok(middlewareStart.ctx.req, 'should have req') + assert.ok(middlewareStart.ctx.res, 'should have res') + assert.ok(middlewareStart.ctx.layer, 'should have layer') + assert.equal(middlewareStart.ctx.layer.name, 'myMiddleware') + + done() + }) + }) + + it('should have layer.name as for unnamed middleware', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.use(function (req, res, next) { + next() + }) + + router.get('/foo', function (req, res) { + res.statusCode = 200 + res.end('hello') + }) + + request(server) + .get('/foo') + .expect(200, function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const anonMiddleware = startEvents.find(function (e) { + return e.ctx.layer && e.ctx.layer.name === '' + }) + + assert.ok(anonMiddleware, 'should have anonymous middleware event') + + done() + }) + }) + }) + + describe('route handler tracing', function () { + it('should have req.route set for route handlers', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/users/:id', function getUser (req, res) { + res.statusCode = 200 + res.end('user') + }) + + request(server) + .get('/users/123') + .expect(200, function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const handlerStart = startEvents.find(function (e) { + return e.ctx.layer && e.ctx.layer.name === 'getUser' + }) + + assert.ok(handlerStart, 'should have start event for getUser') + assert.ok(handlerStart.ctx.req.route, 'should have req.route') + assert.equal(handlerStart.ctx.req.route.path, '/users/:id') + + done() + }) + }) + + it('should not trace the route dispatch wrapper', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/foo', function myHandler (req, res) { + res.statusCode = 200 + res.end('ok') + }) + + request(server) + .get('/foo') + .expect(200, function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const dispatchWrapper = startEvents.find(function (e) { + return e.ctx.layer && e.ctx.layer.name === 'handle' + }) + + assert.ok(!dispatchWrapper, 'should not have dispatch wrapper event') + + done() + }) + }) + }) + + describe('error handler tracing', function () { + it('should trace error handlers (fn.length === 4)', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/fail', function (req, res, next) { + next(new Error('boom')) + }) + + router.use(function myErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + res.statusCode = 500 + res.end(err.message) + }) + + request(server) + .get('/fail') + .expect(500, 'boom', function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const errorHandlerStart = startEvents.find(function (e) { + return e.ctx.layer && e.ctx.layer.name === 'myErrorHandler' + }) + + assert.ok(errorHandlerStart, 'should have start event for error handler') + assert.equal(errorHandlerStart.ctx.layer.handle.length, 4) + + done() + }) + }) + }) + + describe('error channel', function () { + it('should emit error when handler throws synchronously', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/throw', function (req, res) { + throw new Error('sync boom') + }) + + request(server) + .get('/throw') + .expect(500, function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.ok(errorEvents.length > 0, 'should have error events') + + const errorEvent = errorEvents.find(function (e) { + return e.ctx.error && e.ctx.error.message === 'sync boom' + }) + assert.ok(errorEvent, 'should have error event with the thrown error') + + done() + }) + }) + + it('should emit error when async handler rejects', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/reject', async function (req, res) { + throw new Error('async boom') + }) + + request(server) + .get('/reject') + .expect(500, function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.ok(errorEvents.length > 0, 'should have error events') + + const errorEvent = errorEvents.find(function (e) { + return e.ctx.error && e.ctx.error.message === 'async boom' + }) + assert.ok(errorEvent, 'should have error event with the rejected error') + + done() + }) + }) + }) + + describe('async handlers', function () { + it('should trace async handlers that return promises', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/async', function asyncHandler (req, res) { + return new Promise(function (resolve) { + setTimeout(function () { + res.statusCode = 200 + res.end('async hello') + resolve() + }, 10) + }) + }) + + request(server) + .get('/async') + .expect(200, 'async hello', function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const asyncEndEvents = events.filter(function (e) { return e.phase === 'asyncEnd' }) + + assert.ok(startEvents.length > 0, 'should have start events') + assert.ok(asyncEndEvents.length > 0, 'should have asyncEnd events') + + done() + }) + }) + }) + + describe('nested routers', function () { + it('should trace middleware in nested routers', function (done) { + const router = new Router() + const nested = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + nested.get('/bar', function nestedHandler (req, res) { + res.statusCode = 200 + res.end('nested') + }) + + router.use('/foo', nested) + + request(server) + .get('/foo/bar') + .expect(200, 'nested', function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const handlerEvent = startEvents.find(function (e) { + return e.ctx.layer && e.ctx.layer.name === 'nestedHandler' + }) + assert.ok(handlerEvent, 'should have event from nested route handler') + assert.ok(handlerEvent.ctx.req.route, 'should have req.route') + + done() + }) + }) + }) + + describe('event ordering', function () { + it('should emit start before asyncEnd', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/order', function (req, res) { + res.statusCode = 200 + res.end('ok') + }) + + request(server) + .get('/order') + .expect(200, function (err) { + if (err) return done(err) + + const phases = events.map(function (e) { return e.phase }) + const firstStart = phases.indexOf('start') + const lastAsyncEnd = phases.lastIndexOf('asyncEnd') + + assert.ok(firstStart >= 0, 'should have start') + assert.ok(lastAsyncEnd >= 0, 'should have asyncEnd') + assert.ok(firstStart < lastAsyncEnd, 'start should come before asyncEnd') + + done() + }) + }) + }) + + describe('multiple middleware', function () { + it('should emit events for each middleware in the chain', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.use(function first (req, res, next) { + next() + }) + + router.use(function second (req, res, next) { + next() + }) + + router.get('/multi', function handler (req, res) { + res.statusCode = 200 + res.end('multi') + }) + + request(server) + .get('/multi') + .expect(200, function (err) { + if (err) return done(err) + + const startEvents = events.filter(function (e) { return e.phase === 'start' }) + const names = startEvents.map(function (e) { return e.ctx.layer.name }) + + assert.ok(names.indexOf('first') >= 0, 'should trace first middleware') + assert.ok(names.indexOf('second') >= 0, 'should trace second middleware') + + done() + }) + }) + }) +}) From 858399cee8b90aadf4396bb7900ce01d78632f86 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 14 Apr 2026 16:21:38 -0400 Subject: [PATCH 02/13] refactor: reduce duplication in invokeWithTrace Extract handlePromise and unify the traced/untraced paths into a single try/catch and .then handler. --- lib/layer.js | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/lib/layer.js b/lib/layer.js index 98d2ad2..f2beffd 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -176,33 +176,12 @@ Layer.prototype.handleRequest = function handleRequest (req, res, next) { */ function invokeWithTrace (exec, ctxFactory, next) { - if (ctxFactory && shouldTrace(requestChannel)) { - try { - requestChannel.tracePromise(function () { - const ret = exec() - if (isPromise(ret)) { - if (!(ret instanceof Promise)) { - deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') - } - return ret - } - }, ctxFactory()).then(null, function (error) { - next(error || new Error('Rejected promise')) - }) - } catch (err) { - next(err) - } - return - } - try { - const ret = exec() - - if (isPromise(ret)) { - if (!(ret instanceof Promise)) { - deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') - } + const ret = (ctxFactory && shouldTrace(requestChannel)) + ? requestChannel.tracePromise(function () { return handlePromise(exec()) }, ctxFactory()) + : handlePromise(exec()) + if (ret) { ret.then(null, function (error) { next(error || new Error('Rejected promise')) }) @@ -212,6 +191,20 @@ function invokeWithTrace (exec, ctxFactory, next) { } } +/** + * If the return value is a promise, validate it and return it. + * @private + */ + +function handlePromise (ret) { + if (isPromise(ret)) { + if (!(ret instanceof Promise)) { + deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') + } + return ret + } +} + /** * Check if this route matches `path`, if so * populate `.params`. From 9f1aea7764b7590d6d4998f6482e2586863e2287 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Apr 2026 08:27:40 -0400 Subject: [PATCH 03/13] fix: guard against missing dc.tracingChannel on older Node versions --- lib/layer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/layer.js b/lib/layer.js index f2beffd..d617d33 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -31,7 +31,7 @@ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g * @private */ -const requestChannel = dc.tracingChannel('express:request') +const requestChannel = dc.tracingChannel && dc.tracingChannel('express:request') /** * Check if the channel has subscribers. From dea81ae99d5d7870eb1d9dbf66912be4c61cd91c Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Apr 2026 09:23:19 -0400 Subject: [PATCH 04/13] ref: use node: prefix --- lib/layer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/layer.js b/lib/layer.js index d617d33..bdded7c 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -12,7 +12,7 @@ * @private */ -const dc = require('diagnostics_channel') +const dc = require('node:diagnostics_channel') const isPromise = require('is-promise') const pathRegexp = require('path-to-regexp') const debug = require('debug')('router:layer') From 1f6bc25db30bf377845e87179ec01edfbef5a655 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Wed, 15 Apr 2026 09:26:14 -0400 Subject: [PATCH 05/13] fix: use the top-level hasSubscribers flag --- lib/layer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/layer.js b/lib/layer.js index bdded7c..220fa4d 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -37,7 +37,7 @@ const requestChannel = dc.tracingChannel && dc.tracingChannel('express:request') * Check if the channel has subscribers. */ function shouldTrace (ch) { - return ch && ch.start.hasSubscribers !== false + return ch && ch.hasSubscribers !== false } /** From 1ad1b9c31aceca2b696f40da7b7b5857c8316310 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 20 Apr 2026 12:41:21 -0400 Subject: [PATCH 06/13] test: pin down error event semantics for next(err) flows --- test/tracing.js | 244 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/test/tracing.js b/test/tracing.js index c817d04..4a45452 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -211,6 +211,132 @@ describeTracing('TracingChannel', function () { done() }) }) + + it('should not emit error on the originating layer when next(err) is recovered downstream', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/fail', function failingHandler (req, res, next) { + next(new Error('boom')) + }) + + router.use(function myErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + res.statusCode = 500 + res.end(err.message) + }) + + request(server) + .get('/fail') + .expect(500, 'boom', function (err) { + if (err) return done(err) + + const byLayer = function (name) { + return function (e) { return e.ctx.layer && e.ctx.layer.name === name } + } + + const failingEvents = events.filter(byLayer('failingHandler')) + const errorHandlerEvents = events.filter(byLayer('myErrorHandler')) + + assert.ok(failingEvents.some(function (e) { return e.phase === 'start' }), + 'originating layer should have a start event') + assert.ok(!failingEvents.some(function (e) { return e.phase === 'error' }), + 'originating layer should not emit error — next(err) is normal control flow, not an exception') + + assert.ok(errorHandlerEvents.some(function (e) { return e.phase === 'start' }), + 'recovering error handler should have a start event') + assert.ok(!errorHandlerEvents.some(function (e) { return e.phase === 'error' }), + 'recovering error handler should not emit error — it handled the error successfully') + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.equal(errorEvents.length, 0, + 'no error event should fire anywhere when an error is forwarded via next() and recovered downstream') + + done() + }) + }) + + it('should nest the error handler span inside the originating layer span', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.use(function firstMiddleware (req, res, next) { + next() + }) + + router.get('/fail', function failingHandler (req, res, next) { + next(new Error('boom')) + }) + + router.use(function myErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + res.statusCode = 500 + res.end(err.message) + }) + + request(server) + .get('/fail') + .expect(500, 'boom', function (err) { + if (err) return done(err) + + const syncEvents = events.filter(function (e) { + return e.phase === 'start' || e.phase === 'end' + }).map(function (e) { + return e.phase + ':' + (e.ctx.layer && e.ctx.layer.name) + }) + + const failingStart = syncEvents.indexOf('start:failingHandler') + const failingEnd = syncEvents.indexOf('end:failingHandler') + const errorHandlerStart = syncEvents.indexOf('start:myErrorHandler') + const errorHandlerEnd = syncEvents.indexOf('end:myErrorHandler') + + assert.notEqual(failingStart, -1, 'failingHandler should have start') + assert.notEqual(errorHandlerStart, -1, 'myErrorHandler should have start') + + assert.ok(failingStart < errorHandlerStart, + 'failing layer start should come before error handler start') + assert.ok(errorHandlerStart < errorHandlerEnd, + 'error handler start should come before its own end') + assert.ok(errorHandlerEnd < failingEnd, + 'error handler end should come before failing layer end — nesting contract: the error handler that runs via next(err) is nested inside the layer that triggered it, letting APMs attribute the error to the correct parent span') + + done() + }) + }) + + it('should not emit error on the originating layer when next(err) is unhandled', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/fail', function failingHandler (req, res, next) { + next(new Error('unhandled boom')) + }) + + request(server) + .get('/fail') + .expect(500, function (err) { + if (err) return done(err) + + const failingEvents = events.filter(function (e) { + return e.ctx.layer && e.ctx.layer.name === 'failingHandler' + }) + + assert.ok(failingEvents.some(function (e) { return e.phase === 'start' }), + 'originating layer should have a start event') + assert.ok(!failingEvents.some(function (e) { return e.phase === 'error' }), + 'originating layer should not emit error even when no error handler exists — next(err) is not an exception from tracePromise\'s perspective') + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.equal(errorEvents.length, 0, + 'no error event fires on the channel when errors are forwarded via next(); APMs relying on this channel cannot detect next(err) errors') + + done() + }) + }) }) describe('error channel', function () { @@ -267,6 +393,124 @@ describeTracing('TracingChannel', function () { done() }) }) + + it('should emit error on route only when sync throw is recovered by a clean error handler', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/throw', function throwingHandler (req, res) { + throw new Error('sync boom') + }) + + router.use(function cleanErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + res.statusCode = 500 + res.end(err.message) + }) + + request(server) + .get('/throw') + .expect(500, 'sync boom', function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.equal(errorEvents.length, 1, 'exactly one error event should fire') + assert.equal(errorEvents[0].ctx.layer.name, 'throwingHandler', + 'error event should belong to the throwing route layer') + + done() + }) + }) + + it('should emit error on route only when async reject is recovered by a clean error handler', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/reject', async function rejectingHandler (req, res) { + throw new Error('async boom') + }) + + router.use(function cleanErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + res.statusCode = 500 + res.end(err.message) + }) + + request(server) + .get('/reject') + .expect(500, 'async boom', function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.equal(errorEvents.length, 1, 'exactly one error event should fire') + assert.equal(errorEvents[0].ctx.layer.name, 'rejectingHandler', + 'error event should belong to the rejecting route layer') + + done() + }) + }) + + it('should emit error on both layers when sync throw is followed by a throwing error handler', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/throw', function throwingHandler (req, res) { + throw new Error('sync boom') + }) + + router.use(function throwingErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + throw new Error('handler boom') + }) + + request(server) + .get('/throw') + .expect(500, function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + const layerNames = errorEvents.map(function (e) { return e.ctx.layer.name }) + + assert.equal(errorEvents.length, 2, 'error should fire on both layers') + assert.ok(layerNames.includes('throwingHandler'), 'route layer should emit error') + assert.ok(layerNames.includes('throwingErrorHandler'), 'error handler should emit its own error') + + done() + }) + }) + + it('should emit error on both layers when async reject is followed by a throwing error handler', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/reject', async function rejectingHandler (req, res) { + throw new Error('async boom') + }) + + router.use(function throwingErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + throw new Error('handler boom') + }) + + request(server) + .get('/reject') + .expect(500, function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + const layerNames = errorEvents.map(function (e) { return e.ctx.layer.name }) + + assert.equal(errorEvents.length, 2, 'error should fire on both layers') + assert.ok(layerNames.includes('rejectingHandler'), 'route layer should emit error') + assert.ok(layerNames.includes('throwingErrorHandler'), 'error handler should emit its own error') + + done() + }) + }) }) describe('async handlers', function () { From 28b32134587d68028aeda7b548a2df86b967956e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 20 Apr 2026 16:21:08 -0400 Subject: [PATCH 07/13] fix: always publish error events before calling next --- lib/layer.js | 33 +++++++++++++++++-------- test/tracing.js | 64 +++++++++++++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/lib/layer.js b/lib/layer.js index 220fa4d..ab3ecf5 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -127,10 +127,10 @@ Layer.prototype.handleError = function handleError (error, req, res, next) { } const layer = this - invokeWithTrace(function () { - return fn(error, req, res, next) + invokeWithTrace(function (wrappedNext) { + return fn(error, req, res, wrappedNext) }, function () { - return { req, res, layer } + return { req, res, layer, error, handled: true } }, next) } @@ -155,14 +155,14 @@ Layer.prototype.handleRequest = function handleRequest (req, res, next) { // the internal layer that calls route.dispatch). The actual user handlers // inside the route are traced individually. if (this.route) { - return invokeWithTrace(function () { - return fn(req, res, next) + return invokeWithTrace(function (wrappedNext) { + return fn(req, res, wrappedNext) }, null, next) } const layer = this - invokeWithTrace(function () { - return fn(req, res, next) + invokeWithTrace(function (wrappedNext) { + return fn(req, res, wrappedNext) }, function () { return { req, res, layer } }, next) @@ -176,10 +176,23 @@ Layer.prototype.handleRequest = function handleRequest (req, res, next) { */ function invokeWithTrace (exec, ctxFactory, next) { + const tracing = ctxFactory && shouldTrace(requestChannel) + const ctx = tracing ? ctxFactory() : null + const wrappedNext = tracing + ? function (err) { + if (err) { + ctx.error = err + // Explicitly publish the error to the error channel + requestChannel.error.publish(ctx) + } + next(err) + } + : next + try { - const ret = (ctxFactory && shouldTrace(requestChannel)) - ? requestChannel.tracePromise(function () { return handlePromise(exec()) }, ctxFactory()) - : handlePromise(exec()) + const ret = tracing + ? requestChannel.tracePromise(function () { return handlePromise(exec(wrappedNext)) }, ctx) + : handlePromise(exec(wrappedNext)) if (ret) { ret.then(null, function (error) { diff --git a/test/tracing.js b/test/tracing.js index 4a45452..13ce8c0 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -180,13 +180,13 @@ describeTracing('TracingChannel', function () { }) describe('error handler tracing', function () { - it('should trace error handlers (fn.length === 4)', function (done) { + it('should trace error handlers (fn.length === 4) and mark their ctx as handled', function (done) { const router = new Router() const server = createServer(router) dc.tracingChannel('express:request').subscribe(handlers) - router.get('/fail', function (req, res, next) { + router.get('/fail', function failingHandler (req, res, next) { next(new Error('boom')) }) @@ -200,19 +200,35 @@ describeTracing('TracingChannel', function () { .expect(500, 'boom', function (err) { if (err) return done(err) - const startEvents = events.filter(function (e) { return e.phase === 'start' }) - const errorHandlerStart = startEvents.find(function (e) { - return e.ctx.layer && e.ctx.layer.name === 'myErrorHandler' - }) + const byLayer = function (name) { + return function (e) { return e.ctx.layer && e.ctx.layer.name === name } + } + const failingEvents = events.filter(byLayer('failingHandler')) + const errorHandlerEvents = events.filter(byLayer('myErrorHandler')) + + const errorHandlerStart = errorHandlerEvents.find(function (e) { return e.phase === 'start' }) assert.ok(errorHandlerStart, 'should have start event for error handler') assert.equal(errorHandlerStart.ctx.layer.handle.length, 4) + assert.equal(errorHandlerStart.ctx.handled, true, + 'error handler ctx should be marked handled so APMs can dedup the origin error') + assert.ok(errorHandlerStart.ctx.error, + 'error handler ctx should expose the error it received') + + assert.ok(!errorHandlerEvents.some(function (e) { return e.phase === 'error' }), + 'error handler itself did not throw, so it should not emit error') + + const failingError = failingEvents.find(function (e) { return e.phase === 'error' }) + assert.ok(failingError, 'origin layer should emit error for next(err)') + assert.equal(failingError.ctx.error.message, 'boom') + assert.ok(!failingError.ctx.handled, + 'origin layer ctx is not the handler — should not be marked handled') done() }) }) - it('should not emit error on the originating layer when next(err) is recovered downstream', function (done) { + it('should emit error on originating layer when next(err) is recovered downstream', function (done) { const router = new Router() const server = createServer(router) @@ -239,19 +255,21 @@ describeTracing('TracingChannel', function () { const failingEvents = events.filter(byLayer('failingHandler')) const errorHandlerEvents = events.filter(byLayer('myErrorHandler')) - assert.ok(failingEvents.some(function (e) { return e.phase === 'start' }), - 'originating layer should have a start event') - assert.ok(!failingEvents.some(function (e) { return e.phase === 'error' }), - 'originating layer should not emit error — next(err) is normal control flow, not an exception') + const failingError = failingEvents.find(function (e) { return e.phase === 'error' }) + assert.ok(failingError, + 'originating layer should emit error — unhandled-at-origin is always observable') + assert.equal(failingError.ctx.error.message, 'boom') - assert.ok(errorHandlerEvents.some(function (e) { return e.phase === 'start' }), - 'recovering error handler should have a start event') assert.ok(!errorHandlerEvents.some(function (e) { return e.phase === 'error' }), - 'recovering error handler should not emit error — it handled the error successfully') + 'recovering error handler itself did not throw — should not emit error') + + const errorHandlerStart = errorHandlerEvents.find(function (e) { return e.phase === 'start' }) + assert.equal(errorHandlerStart.ctx.handled, true, + 'error handler ctx is marked handled so APMs can dedup against the origin error') const errorEvents = events.filter(function (e) { return e.phase === 'error' }) - assert.equal(errorEvents.length, 0, - 'no error event should fire anywhere when an error is forwarded via next() and recovered downstream') + assert.equal(errorEvents.length, 1, + 'exactly one error event fires — on the origin layer that called next(err)') done() }) @@ -306,7 +324,7 @@ describeTracing('TracingChannel', function () { }) }) - it('should not emit error on the originating layer when next(err) is unhandled', function (done) { + it('should emit error on originating layer when next(err) is unhandled', function (done) { const router = new Router() const server = createServer(router) @@ -325,14 +343,14 @@ describeTracing('TracingChannel', function () { return e.ctx.layer && e.ctx.layer.name === 'failingHandler' }) - assert.ok(failingEvents.some(function (e) { return e.phase === 'start' }), - 'originating layer should have a start event') - assert.ok(!failingEvents.some(function (e) { return e.phase === 'error' }), - 'originating layer should not emit error even when no error handler exists — next(err) is not an exception from tracePromise\'s perspective') + const failingError = failingEvents.find(function (e) { return e.phase === 'error' }) + assert.ok(failingError, + 'unhandled next(err) must be observable on the origin layer') + assert.equal(failingError.ctx.error.message, 'unhandled boom') const errorEvents = events.filter(function (e) { return e.phase === 'error' }) - assert.equal(errorEvents.length, 0, - 'no error event fires on the channel when errors are forwarded via next(); APMs relying on this channel cannot detect next(err) errors') + assert.equal(errorEvents.length, 1, + 'exactly one error event fires — on the origin layer that called next(err)') done() }) From 90ed7e2cf75e8da027d9a47763c96471457aa34e Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 20 Apr 2026 16:29:56 -0400 Subject: [PATCH 08/13] fix: refine error handling for control-flow sentinels in invokeWithTrace --- lib/layer.js | 4 +++- test/tracing.js | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/layer.js b/lib/layer.js index ab3ecf5..ca571ec 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -180,7 +180,9 @@ function invokeWithTrace (exec, ctxFactory, next) { const ctx = tracing ? ctxFactory() : null const wrappedNext = tracing ? function (err) { - if (err) { + // 'route' and 'router' are control-flow sentinels (skip route / exit router), + // not real errors — don't pollute the error channel with them. + if (err && err !== 'route' && err !== 'router') { ctx.error = err // Explicitly publish the error to the error channel requestChannel.error.publish(ctx) diff --git a/test/tracing.js b/test/tracing.js index 13ce8c0..eda5967 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -324,6 +324,34 @@ describeTracing('TracingChannel', function () { }) }) + it('should not emit error for next("route") or next("router") control-flow sentinels', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.get('/skip', function skipToNextRoute (req, res, next) { + next('route') + }) + + router.get('/skip', function nextRouteHandler (req, res) { + res.statusCode = 200 + res.end('skipped') + }) + + request(server) + .get('/skip') + .expect(200, 'skipped', function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.equal(errorEvents.length, 0, + 'next("route") is a control-flow sentinel, not an error — nothing should publish') + + done() + }) + }) + it('should emit error on originating layer when next(err) is unhandled', function (done) { const router = new Router() const server = createServer(router) @@ -480,7 +508,8 @@ describeTracing('TracingChannel', function () { throw new Error('sync boom') }) - router.use(function throwingErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + // eslint-disable-next-line no-unused-vars, n/handle-callback-err + router.use(function throwingErrorHandler (err, req, res, next) { throw new Error('handler boom') }) @@ -510,7 +539,8 @@ describeTracing('TracingChannel', function () { throw new Error('async boom') }) - router.use(function throwingErrorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars + // eslint-disable-next-line no-unused-vars, n/handle-callback-err + router.use(function throwingErrorHandler (err, req, res, next) { throw new Error('handler boom') }) From 758030ee576d84053167cf4412e155f000393260 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 20 Apr 2026 16:31:31 -0400 Subject: [PATCH 09/13] test: added case for router control flow error --- test/tracing.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/tracing.js b/test/tracing.js index eda5967..98627b2 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -324,7 +324,7 @@ describeTracing('TracingChannel', function () { }) }) - it('should not emit error for next("route") or next("router") control-flow sentinels', function (done) { + it('should not emit error for next("route") control-flow sentinel', function (done) { const router = new Router() const server = createServer(router) @@ -352,6 +352,34 @@ describeTracing('TracingChannel', function () { }) }) + it('should not emit error for next("router") control-flow sentinel', function (done) { + const router = new Router() + const server = createServer(router) + + dc.tracingChannel('express:request').subscribe(handlers) + + router.use(function ejectFromRouter (req, res, next) { + next('router') + }) + + router.get('/foo', function shouldNotRun (req, res) { + res.statusCode = 200 + res.end('should not reach') + }) + + request(server) + .get('/foo') + .expect(404, function (err) { + if (err) return done(err) + + const errorEvents = events.filter(function (e) { return e.phase === 'error' }) + assert.equal(errorEvents.length, 0, + 'next("router") is a control-flow sentinel, not an error — nothing should publish') + + done() + }) + }) + it('should emit error on originating layer when next(err) is unhandled', function (done) { const router = new Router() const server = createServer(router) From 9c9b4a77eed2ec01a7e6c804ccd70ed89c67b66b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 20 Apr 2026 16:46:27 -0400 Subject: [PATCH 10/13] test: assert handled flag on error events in both-layers error tests --- test/tracing.js | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/test/tracing.js b/test/tracing.js index 98627b2..16ddd04 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -547,11 +547,21 @@ describeTracing('TracingChannel', function () { if (err) return done(err) const errorEvents = events.filter(function (e) { return e.phase === 'error' }) - const layerNames = errorEvents.map(function (e) { return e.ctx.layer.name }) + const byName = function (name) { + return errorEvents.find(function (e) { return e.ctx.layer.name === name }) + } assert.equal(errorEvents.length, 2, 'error should fire on both layers') - assert.ok(layerNames.includes('throwingHandler'), 'route layer should emit error') - assert.ok(layerNames.includes('throwingErrorHandler'), 'error handler should emit its own error') + + const routeError = byName('throwingHandler') + assert.ok(routeError, 'route layer should emit error') + assert.ok(!routeError.ctx.handled, + 'route layer is not an error handler — handled flag must be absent') + + const handlerError = byName('throwingErrorHandler') + assert.ok(handlerError, 'error handler should emit its own error') + assert.equal(handlerError.ctx.handled, true, + 'error handler\'s own error event must carry handled:true so APMs can classify the span correctly') done() }) @@ -578,11 +588,21 @@ describeTracing('TracingChannel', function () { if (err) return done(err) const errorEvents = events.filter(function (e) { return e.phase === 'error' }) - const layerNames = errorEvents.map(function (e) { return e.ctx.layer.name }) + const byName = function (name) { + return errorEvents.find(function (e) { return e.ctx.layer.name === name }) + } assert.equal(errorEvents.length, 2, 'error should fire on both layers') - assert.ok(layerNames.includes('rejectingHandler'), 'route layer should emit error') - assert.ok(layerNames.includes('throwingErrorHandler'), 'error handler should emit its own error') + + const routeError = byName('rejectingHandler') + assert.ok(routeError, 'route layer should emit error') + assert.ok(!routeError.ctx.handled, + 'route layer is not an error handler — handled flag must be absent') + + const handlerError = byName('throwingErrorHandler') + assert.ok(handlerError, 'error handler should emit its own error') + assert.equal(handlerError.ctx.handled, true, + 'error handler\'s own error event must carry handled:true so APMs can classify the span correctly') done() }) From 67c3851c28e8f0b43a41a5fe62c27d1a1a4da4cd Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Tue, 21 Apr 2026 11:19:13 -0400 Subject: [PATCH 11/13] refactor: rename tracing channel to pillarjs.router.request Follows the Node.js TracingChannel naming guideline (dot-separated, module-scoped) and avoids the express-specific prefix since router is usable outside express. --- lib/layer.js | 2 +- test/tracing.js | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/layer.js b/lib/layer.js index ca571ec..e243b9a 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -31,7 +31,7 @@ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g * @private */ -const requestChannel = dc.tracingChannel && dc.tracingChannel('express:request') +const requestChannel = dc.tracingChannel && dc.tracingChannel('pillarjs.router.request') /** * Check if the channel has subscribers. diff --git a/test/tracing.js b/test/tracing.js index 16ddd04..8d9fecc 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -34,7 +34,7 @@ describeTracing('TracingChannel', function () { }) afterEach(function () { - dc.tracingChannel('express:request').unsubscribe(handlers) + dc.tracingChannel('pillarjs.router.request').unsubscribe(handlers) }) describe('when no subscribers', function () { @@ -58,7 +58,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.use(function myMiddleware (req, res, next) { next() @@ -93,7 +93,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.use(function (req, res, next) { next() @@ -126,7 +126,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/users/:id', function getUser (req, res) { res.statusCode = 200 @@ -155,7 +155,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/foo', function myHandler (req, res) { res.statusCode = 200 @@ -184,7 +184,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/fail', function failingHandler (req, res, next) { next(new Error('boom')) @@ -232,7 +232,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/fail', function failingHandler (req, res, next) { next(new Error('boom')) @@ -279,7 +279,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.use(function firstMiddleware (req, res, next) { next() @@ -328,7 +328,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/skip', function skipToNextRoute (req, res, next) { next('route') @@ -356,7 +356,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.use(function ejectFromRouter (req, res, next) { next('router') @@ -384,7 +384,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/fail', function failingHandler (req, res, next) { next(new Error('unhandled boom')) @@ -418,7 +418,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/throw', function (req, res) { throw new Error('sync boom') @@ -445,7 +445,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/reject', async function (req, res) { throw new Error('async boom') @@ -472,7 +472,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/throw', function throwingHandler (req, res) { throw new Error('sync boom') @@ -501,7 +501,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/reject', async function rejectingHandler (req, res) { throw new Error('async boom') @@ -530,7 +530,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/throw', function throwingHandler (req, res) { throw new Error('sync boom') @@ -571,7 +571,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/reject', async function rejectingHandler (req, res) { throw new Error('async boom') @@ -614,7 +614,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/async', function asyncHandler (req, res) { return new Promise(function (resolve) { @@ -648,7 +648,7 @@ describeTracing('TracingChannel', function () { const nested = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) nested.get('/bar', function nestedHandler (req, res) { res.statusCode = 200 @@ -679,7 +679,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.get('/order', function (req, res) { res.statusCode = 200 @@ -709,7 +709,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('express:request').subscribe(handlers) + dc.tracingChannel('pillarjs.router.request').subscribe(handlers) router.use(function first (req, res, next) { next() From 6aa0575a2c8132ca55db4179a9eff51d4a8d9ebd Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sun, 26 Apr 2026 20:02:02 -0400 Subject: [PATCH 12/13] docs: document pillarjs.router.request tracing channel --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 156c380..991a522 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,56 @@ router.route('/pet/:id') server.listen(8080) ``` +## Diagnostics + +`router` integrates with Node.js [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) +via a [`TracingChannel`](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel) +named `pillarjs.router.request`. This lets observability tools (APMs, tracers, +loggers) hook into middleware and route handler execution without monkey-patching. + +Each layer's handler invocation publishes the standard tracing channel sub-events +(`start`, `end`, `asyncStart`, `asyncEnd`, `error`). The published context object +contains: + +- `req`: the incoming `http.IncomingMessage` +- `res`: the `http.ServerResponse` +- `layer`: the internal `Layer` instance being invoked (exposes `.name`, `.path`, `.handle`, etc.). Note that `Layer` is an internal implementation detail and its shape may change between releases. +- `error`: the error passed to `next(err)`, when applicable +- `handled`: `true` when the layer is an error-handling middleware (4-arg signature) + +The `error` event is also published when a handler calls `next(err)` with a real +error. The control-flow sentinels `'route'` and `'router'` are not treated as +errors and will not publish to the `error` channel. + +When no subscribers are attached, tracing is bypassed entirely, so there is no +context allocation or channel publishing overhead on the hot path. + +```js +const dc = require('node:diagnostics_channel') + +const channel = dc.tracingChannel('pillarjs.router.request') + +channel.subscribe({ + start (ctx) { + ctx.startTime = process.hrtime.bigint() + }, + end (ctx) { + // do whatever you need on synchronous completion + }, + asyncStart (ctx) { + // do whatever you need when the async portion begins + }, + asyncEnd (ctx) { + const durationNs = process.hrtime.bigint() - ctx.startTime + console.log('%s %s -> %s (%dns)', + ctx.req.method, ctx.req.url, ctx.layer.name, durationNs) + }, + error (ctx) { + console.error('handler error in %s:', ctx.layer.name, ctx.error) + } +}) +``` + ## License [MIT](LICENSE) From 5e800782aebb82a2e87014656e3810abb06a0fce Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 27 Apr 2026 11:01:47 -0400 Subject: [PATCH 13/13] refactor: rename tracing channel to express.router.request --- README.md | 4 ++-- lib/layer.js | 2 +- test/tracing.js | 42 +++++++++++++++++++++--------------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 991a522..a15dac2 100644 --- a/README.md +++ b/README.md @@ -404,7 +404,7 @@ server.listen(8080) `router` integrates with Node.js [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) via a [`TracingChannel`](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel) -named `pillarjs.router.request`. This lets observability tools (APMs, tracers, +named `express.router.request`. This lets observability tools (APMs, tracers, loggers) hook into middleware and route handler execution without monkey-patching. Each layer's handler invocation publishes the standard tracing channel sub-events @@ -427,7 +427,7 @@ context allocation or channel publishing overhead on the hot path. ```js const dc = require('node:diagnostics_channel') -const channel = dc.tracingChannel('pillarjs.router.request') +const channel = dc.tracingChannel('express.router.request') channel.subscribe({ start (ctx) { diff --git a/lib/layer.js b/lib/layer.js index e243b9a..6744db4 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -31,7 +31,7 @@ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g * @private */ -const requestChannel = dc.tracingChannel && dc.tracingChannel('pillarjs.router.request') +const requestChannel = dc.tracingChannel && dc.tracingChannel('express.router.request') /** * Check if the channel has subscribers. diff --git a/test/tracing.js b/test/tracing.js index 8d9fecc..c1e1d96 100644 --- a/test/tracing.js +++ b/test/tracing.js @@ -34,7 +34,7 @@ describeTracing('TracingChannel', function () { }) afterEach(function () { - dc.tracingChannel('pillarjs.router.request').unsubscribe(handlers) + dc.tracingChannel('express.router.request').unsubscribe(handlers) }) describe('when no subscribers', function () { @@ -58,7 +58,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.use(function myMiddleware (req, res, next) { next() @@ -93,7 +93,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.use(function (req, res, next) { next() @@ -126,7 +126,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/users/:id', function getUser (req, res) { res.statusCode = 200 @@ -155,7 +155,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/foo', function myHandler (req, res) { res.statusCode = 200 @@ -184,7 +184,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/fail', function failingHandler (req, res, next) { next(new Error('boom')) @@ -232,7 +232,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/fail', function failingHandler (req, res, next) { next(new Error('boom')) @@ -279,7 +279,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.use(function firstMiddleware (req, res, next) { next() @@ -328,7 +328,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/skip', function skipToNextRoute (req, res, next) { next('route') @@ -356,7 +356,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.use(function ejectFromRouter (req, res, next) { next('router') @@ -384,7 +384,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/fail', function failingHandler (req, res, next) { next(new Error('unhandled boom')) @@ -418,7 +418,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/throw', function (req, res) { throw new Error('sync boom') @@ -445,7 +445,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/reject', async function (req, res) { throw new Error('async boom') @@ -472,7 +472,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/throw', function throwingHandler (req, res) { throw new Error('sync boom') @@ -501,7 +501,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/reject', async function rejectingHandler (req, res) { throw new Error('async boom') @@ -530,7 +530,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/throw', function throwingHandler (req, res) { throw new Error('sync boom') @@ -571,7 +571,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/reject', async function rejectingHandler (req, res) { throw new Error('async boom') @@ -614,7 +614,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/async', function asyncHandler (req, res) { return new Promise(function (resolve) { @@ -648,7 +648,7 @@ describeTracing('TracingChannel', function () { const nested = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) nested.get('/bar', function nestedHandler (req, res) { res.statusCode = 200 @@ -679,7 +679,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.get('/order', function (req, res) { res.statusCode = 200 @@ -709,7 +709,7 @@ describeTracing('TracingChannel', function () { const router = new Router() const server = createServer(router) - dc.tracingChannel('pillarjs.router.request').subscribe(handlers) + dc.tracingChannel('express.router.request').subscribe(handlers) router.use(function first (req, res, next) { next()