Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
95e23ba
feat: add TracingChannel support for express:request
logaretm Apr 14, 2026
858399c
refactor: reduce duplication in invokeWithTrace
logaretm Apr 14, 2026
9f1aea7
fix: guard against missing dc.tracingChannel on older Node versions
logaretm Apr 15, 2026
dea81ae
ref: use node: prefix
logaretm Apr 15, 2026
1f6bc25
fix: use the top-level hasSubscribers flag
logaretm Apr 15, 2026
1ad1b9c
test: pin down error event semantics for next(err) flows
logaretm Apr 20, 2026
28b3213
fix: always publish error events before calling next
logaretm Apr 20, 2026
90ed7e2
fix: refine error handling for control-flow sentinels in invokeWithTrace
logaretm Apr 20, 2026
758030e
test: added case for router control flow error
logaretm Apr 20, 2026
9c9b4a7
test: assert handled flag on error events in both-layers error tests
logaretm Apr 20, 2026
67c3851
refactor: rename tracing channel to pillarjs.router.request
logaretm Apr 21, 2026
6aa0575
docs: document pillarjs.router.request tracing channel
logaretm Apr 27, 2026
5e80078
refactor: rename tracing channel to express.router.request
logaretm Apr 27, 2026
da0da14
fix: detect tracing subscribers on Node versions without TracingChann…
logaretm Jul 15, 2026
8fd857f
refactor: gate tracing on subscribers in callers and pass ctx directly
logaretm Jul 15, 2026
337e212
fix: report each request error once, at its origin layer
logaretm Jul 15, 2026
232143b
fix: don't report thrown 'route'/'router' signals as errors
logaretm Jul 15, 2026
10a47bc
docs: clarify error semantics on the request tracing channel
logaretm Jul 15, 2026
cc5deb7
fix: report value-less rejections as the forwarded error on the channel
logaretm Jul 15, 2026
9aa666a
test: extract channel-name constant and shared byLayer helper
logaretm Jul 15, 2026
04b2f72
fix: route traced outcomes through next so tracing never changes routing
logaretm Jul 15, 2026
97da6d8
test: hoist repeated setup and predicates into shared helpers
logaretm Jul 15, 2026
583c8c4
refactor: rename the error-handler context flag to errorHandler
logaretm Jul 15, 2026
d0d8f7e
perf: publish lifecycle via start.runStores instead of tracePromise
logaretm Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 64 additions & 25 deletions lib/layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* @private
*/

const dc = require('node:diagnostics_channel')
const isPromise = require('is-promise')
const pathRegexp = require('path-to-regexp')
const debug = require('debug')('router:layer')
Expand All @@ -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 && dc.tracingChannel('express:request')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is express:request channel used here?
I think it would be better to use a dedicated channel for router to allow consumers to decide on what to receive and how to handle it.
I think router component can be also used without express.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I don't think it has to be express prefixed but it needs a unique prefix for sure which is why I wanted some opinions on what it should be. My concern is router:request is too generic and can easily conflict with other names in the ecosystem.

I had this in the PR description:

The channel name right now is express:request, in the original PR it was request but I think channel names need to have a relationship with the module name otherwise we risk duplication in the ecosystem.

WDYT? Maybe pillarjs:router:request?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a guideline.
So I guess the string to use would be pillarjs.router.request as tracing: prefix and event name postfix is added by dc.tracingChannel().

@logaretm logaretm Apr 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem like there is a consistent standard, docs do suggest that but others just went with lib:something:action formats.

Anyways I have no strong opinions here and I think as long as the name is unique enough, it is arbitrary. I renamed it to your suggestion 👍

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should just be router.request and not pillarjs.router.request. I say this because pillarjs is more like the organization name and that could change, and we might move this package to the expressjs organization (not saying we’ll do it now, but it could happen, so we shouldn’t be tied to the GitHub organization name).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I understand the concern. In that case, instead of using pillarjs, it’s better to use express, since it’s officially under the expressjs project.

How does Fastify handle it?

@logaretm logaretm Apr 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should we go for express.request or express.router.request? To me its arbitrary but perhaps you have a strong opinion?

Fastify uses fastify.request.handler as seen here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think express.request is a bit too generic. There might be other parts in express describing the request lifecycle where the router component is not included.
e.g. express.request.router or express.router.request - tending towards express.request.router like fastify and hierarchic ordering.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with express.router.request since it describes the action better "Express's router is handling a request".

But happy to change to whatever that is acceptable to get this through.


/**
* Check if the channel has subscribers.
*/
function shouldTrace (ch) {
return ch && ch.hasSubscribers !== false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we support Node 18.0.0, users on those versions will get true, because (undefined !== false) === true.

https://nodejs.org/api/diagnostics_channel.html#diagnostics_channeltracingchannelnameorchannels

@logaretm logaretm Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep this is a trade off I suppose. I think we should then check for all channels own subscribers in that case.

}

/**
* Expose `Layer`.
*/
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -147,16 +151,37 @@ Layer.prototype.handleRequest = function handleRequest (req, res, next) {
return next()
}

try {
// invoke function
const ret = fn(req, res, 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)
}

// wait for returned promise
if (isPromise(ret)) {
if (!(ret instanceof Promise)) {
deprecate('handlers that are Promise-like are deprecated, use a native Promise instead')
}
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) {
try {
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'))
})
Expand All @@ -166,6 +191,20 @@ Layer.prototype.handleRequest = function handleRequest (req, res, 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`.
Expand Down
Loading