Skip to content

Commit 0b0492f

Browse files
committed
fix: Updated express instrumentation to ignore error when the next handler passes route or router
1 parent 5f7bbb8 commit 0b0492f

File tree

8 files changed

+146
-16
lines changed

8 files changed

+146
-16
lines changed

lib/subscribers/express/base.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
*/
15+
function errorHandler(err) {
16+
return err && err !== 'route' && err !== 'router'
17+
}
18+
19+
class ExpressSubscriber extends MiddlewareSubscriber {
20+
constructor({ agent, logger, packageName = 'express', channelName }) {
21+
super({ agent, logger, packageName, channelName, system: 'Expressjs', errorHandler })
22+
}
23+
}
24+
25+
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: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,24 @@ 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+
*/
28+
function defaultErrorHandler(_err) {
29+
return true
30+
}
31+
2232
/**
2333
* The baseline parameters available to the middleware wrapper
2434
*
2535
* @typedef {object} WrapperParams
2636
* @property {object} agent A New Relic Node.js agent instance.
2737
* @property {object} logger An agent logger instance.
2838
* @property {string} system handling the instrumentation(i.e - Fastify, Expressjs, Hapi, etc)
39+
* @property {Function} [errorHandler] optional function to determine if error should be recorded
2940
*/
3041

3142
/**
@@ -36,13 +47,14 @@ function getFunctionName(handler) {
3647
* @property {string} prefix formatted prefix to name segments/timeslice metrics
3748
*/
3849
class MiddlewareWrapper {
39-
constructor({ agent, logger, system }) {
50+
constructor({ agent, logger, system, errorHandler }) {
4051
this.agent = agent
4152
this.logger = logger
4253
this.system = system
4354
this.config = agent.config
4455
this.prefix = `Nodejs/Middleware/${this.system}`
4556
this.agent.environment.setFramework(system)
57+
this.isError = errorHandler ?? defaultErrorHandler
4658
}
4759

4860
/**
@@ -180,7 +192,7 @@ class MiddlewareWrapper {
180192
if (isSync) {
181193
function wrappedDone(...doneArgs) {
182194
const [err] = doneArgs
183-
if (err) {
195+
if (self.isError(err)) {
184196
self.storeError(txInfo, err)
185197
// route has been completed, pop from path
186198
// 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: 47 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,44 @@ 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 nothandle error when isError is not using default handler', function (t, end) {
100+
const { agent, txInfo, wrapper } = t.nr
101+
const error = new Error('test error')
102+
function handler (req, res, next) {
103+
next(error)
104+
}
105+
const route = '/test/url'
106+
const wrapped = wrapper.wrap({ handler, route })
107+
wrapper.isError = function(_err) {
108+
return false
109+
}
110+
helper.runInTransaction(agent, function (tx) {
111+
tx.type = 'web'
112+
tx.url = route
113+
wrapped('one', 'two', function() {})
114+
assert.deepEqual(txInfo, {})
115+
tx.end()
116+
end()
117+
})
118+
})

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)