Skip to content

Commit 724b218

Browse files
authored
fix: Updated express instrumentation to ignore error when the next handler passes route or router (#3513)
1 parent ef41b38 commit 724b218

File tree

8 files changed

+166
-16
lines changed

8 files changed

+166
-16
lines changed

lib/subscribers/express/base.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const MiddlewareSubscriber = require('../middleware')
7+
8+
/**
9+
* Special error handler to ignore if the error passed to next handler
10+
* is 'route' or 'router', which are used by Express internally to
11+
* skip out of a route or router.
12+
*
13+
* @param {*} err - The error passed to the next handler
14+
* @returns {boolean} - returns true if error exists and not equal to a string or `route` or `router`
15+
*/
16+
function errorHandler(err) {
17+
return err && err !== 'route' && err !== 'router'
18+
}
19+
20+
class ExpressSubscriber extends MiddlewareSubscriber {
21+
constructor({ agent, logger, packageName = 'express', channelName }) {
22+
super({ agent, logger, packageName, channelName, system: 'Expressjs', errorHandler })
23+
}
24+
}
25+
26+
module.exports = ExpressSubscriber

lib/subscribers/express/param.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
'use strict'
77

8-
const MiddlewareSubscriber = require('../middleware')
8+
const ExpressSubscriber = require('./base')
99

10-
class ExpressParamSubscriber extends MiddlewareSubscriber {
11-
constructor({ agent, logger, packageName = 'express' }) {
12-
super({ agent, logger, packageName, channelName: 'nr_param', system: 'Expressjs' })
10+
class ExpressParamSubscriber extends ExpressSubscriber {
11+
constructor({ agent, logger, packageName }) {
12+
super({ agent, logger, packageName, channelName: 'nr_param' })
1313
}
1414

1515
handler(data) {

lib/subscribers/express/route.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
'use strict'
77

8-
const MiddlewareSubscriber = require('../middleware')
98
const methods = ['all', 'delete', 'get', 'head', 'post', 'put', 'patch']
9+
const ExpressSubscriber = require('./base')
1010

11-
class ExpressRouteSubscriber extends MiddlewareSubscriber {
12-
constructor({ agent, logger, packageName = 'express' }) {
13-
super({ agent, logger, packageName, channelName: 'nr_route', system: 'Expressjs' })
11+
class ExpressRouteSubscriber extends ExpressSubscriber {
12+
constructor({ agent, logger, packageName }) {
13+
super({ agent, logger, packageName, channelName: 'nr_route' })
1414
this.events = ['end']
1515
}
1616

lib/subscribers/express/use.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
'use strict'
77

8-
const MiddlewareSubscriber = require('../middleware')
8+
const ExpressSubscriber = require('./base')
99

10-
class ExpressUseSubscriber extends MiddlewareSubscriber {
11-
constructor({ agent, logger, packageName = 'express' }) {
12-
super({ agent, logger, packageName, channelName: 'nr_use', system: 'Expressjs' })
10+
class ExpressUseSubscriber extends ExpressSubscriber {
11+
constructor({ agent, logger, packageName }) {
12+
super({ agent, logger, packageName, channelName: 'nr_use' })
1313
}
1414

1515
handler(data) {

lib/subscribers/middleware-wrapper.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,25 @@ function getFunctionName(handler) {
1919
return handler.name === '' ? '<anonymous>' : handler.name
2020
}
2121

22+
/**
23+
* Default error handler for determining if error should stored with transaction.
24+
* Note: Based on previous shim instrumentation only Express, Restify, and Hapi have
25+
* a different handler.
26+
* @param {Error} err error passed to done handler
27+
* @returns {boolean} returns true if error exists
28+
*/
29+
function defaultErrorHandler(err) {
30+
return err
31+
}
32+
2233
/**
2334
* The baseline parameters available to the middleware wrapper
2435
*
2536
* @typedef {object} WrapperParams
2637
* @property {object} agent A New Relic Node.js agent instance.
2738
* @property {object} logger An agent logger instance.
2839
* @property {string} system handling the instrumentation(i.e - Fastify, Expressjs, Hapi, etc)
40+
* @property {Function} [errorHandler] optional function to determine if error should be recorded
2941
*/
3042

3143
/**
@@ -36,13 +48,14 @@ function getFunctionName(handler) {
3648
* @property {string} prefix formatted prefix to name segments/timeslice metrics
3749
*/
3850
class MiddlewareWrapper {
39-
constructor({ agent, logger, system }) {
51+
constructor({ agent, logger, system, errorHandler }) {
4052
this.agent = agent
4153
this.logger = logger
4254
this.system = system
4355
this.config = agent.config
4456
this.prefix = `Nodejs/Middleware/${this.system}`
4557
this.agent.environment.setFramework(system)
58+
this.isError = errorHandler ?? defaultErrorHandler
4659
}
4760

4861
/**
@@ -180,7 +193,7 @@ class MiddlewareWrapper {
180193
if (isSync) {
181194
function wrappedDone(...doneArgs) {
182195
const [err] = doneArgs
183-
if (err) {
196+
if (self.isError(err)) {
184197
self.storeError(txInfo, err)
185198
// route has been completed, pop from path
186199
// to allow other handlers to name it more accurately

lib/subscribers/middleware.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ const Subscriber = require('./base')
88
const MiddlewareWrapper = require('./middleware-wrapper')
99

1010
class MiddlewareSubscriber extends Subscriber {
11-
constructor({ agent, logger, packageName, channelName, system }) {
11+
constructor({ agent, logger, packageName, channelName, system, errorHandler }) {
1212
super({ agent, logger, packageName, channelName })
1313
// this is because the handler simply wraps a function
1414
// that is executed later when a request is made
1515
this.requireActiveTx = false
16-
this.wrapper = new MiddlewareWrapper({ agent, logger, system })
16+
this.wrapper = new MiddlewareWrapper({ agent, logger, system, errorHandler })
1717
}
1818
}
1919

test/unit/lib/subscribers/middleare-wrapper.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ test.beforeEach((ctx) => {
1717
function handler (arg1, arg2, arg3) {
1818
return `${arg1}, ${arg2}, ${arg3}`
1919
}
20+
const txInfo = {}
21+
wrapper.extractTxInfo = function() {
22+
const request = {}
23+
return { txInfo, request, errorWare: false }
24+
}
2025
ctx.nr = {
2126
agent,
2227
handler,
2328
logger,
29+
txInfo,
2430
wrapper
2531
}
2632
})
@@ -69,3 +75,62 @@ test('should run wrapped handler in context if transaction present, and properly
6975
end()
7076
})
7177
})
78+
79+
test('should handle error when passed in to done handler', function (t, end) {
80+
const { agent, txInfo, wrapper } = t.nr
81+
const error = new Error('test error')
82+
function handler (req, res, next) {
83+
next(error)
84+
}
85+
const route = '/test/url'
86+
const wrapped = wrapper.wrap({ handler, route })
87+
helper.runInTransaction(agent, function (tx) {
88+
tx.type = 'web'
89+
tx.url = route
90+
wrapped('one', 'two', function() {})
91+
assert.deepEqual(txInfo.error, error)
92+
assert.equal(txInfo.errorHandled, false)
93+
94+
tx.end()
95+
end()
96+
})
97+
})
98+
99+
test('should not handle error when no error is passed to done handler', function (t, end) {
100+
const { agent, txInfo, wrapper } = t.nr
101+
function handler (req, res, next) {
102+
next()
103+
}
104+
const route = '/test/url'
105+
const wrapped = wrapper.wrap({ handler, route })
106+
helper.runInTransaction(agent, function (tx) {
107+
tx.type = 'web'
108+
tx.url = route
109+
wrapped('one', 'two', function() {})
110+
assert.deepEqual(txInfo, {})
111+
112+
tx.end()
113+
end()
114+
})
115+
})
116+
117+
test('should not handle error when isError is not using default handler', function (t, end) {
118+
const { agent, txInfo, wrapper } = t.nr
119+
const error = new Error('test error')
120+
function handler (req, res, next) {
121+
next(error)
122+
}
123+
const route = '/test/url'
124+
const wrapped = wrapper.wrap({ handler, route })
125+
wrapper.isError = function(err) {
126+
return err.message !== 'test error'
127+
}
128+
helper.runInTransaction(agent, function (tx) {
129+
tx.type = 'web'
130+
tx.url = route
131+
wrapped('one', 'two', function() {})
132+
assert.deepEqual(txInfo, {})
133+
tx.end()
134+
end()
135+
})
136+
})

test/versioned/express/errors.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,52 @@ test('Error handling tests', async (t) => {
224224
})
225225
await plan.completed
226226
})
227+
228+
await t.test('does not report error when the argument to next is `route`', function (t, end) {
229+
const { app, express } = t.nr
230+
231+
const router1 = express.Router()
232+
function bail(req, res, next) {
233+
next('route')
234+
}
235+
router1.get('/test', bail, function (req, res, next) {
236+
res.send('fail')
237+
})
238+
239+
app.use(router1)
240+
app.get('/test', function (req, res) {
241+
res.send('done')
242+
})
243+
244+
runTest(t, function (errors, statuscode) {
245+
assert.equal(errors.length, 0)
246+
assert.equal(statuscode, 200)
247+
end()
248+
})
249+
})
250+
251+
await t.test('does not report error when the argument to next is `router`', function (t, end) {
252+
const { app, express } = t.nr
253+
254+
const router1 = express.Router()
255+
function bail(req, res, next) {
256+
next('router')
257+
}
258+
router1.get('/test', bail, function (req, res, next) {
259+
res.end('fail')
260+
})
261+
262+
app.use(router1)
263+
app.get('/test', function (req, res) {
264+
res.end('done')
265+
})
266+
267+
runTest(t, function (errors, statuscode) {
268+
assert.equal(errors.length, 0)
269+
assert.equal(statuscode, 200)
270+
end()
271+
})
272+
})
227273
})
228274

229275
function runTest(t, callback) {

0 commit comments

Comments
 (0)