Skip to content

Commit 0938282

Browse files
authored
fix(restify): support an array of handlers (#709)
If an array of handlers is given as an argument to `server.use` instead of just a single function, the agent would cause an exception to be raised. This commit changes the Restify instrumentation logic so that the handlers no longer needs to be wrapped, and so that the arguments to the `server.use` function no longer needs to be parsed by the agent. Instead the agent now patches a central (private) error handling function to be notified when ever an error is passed to the `next` function. The route name is instead gathered by the same logic that also gathers the route name for Express - from `req.route.path` - as Restify just so happens to use the same API for storing the route name on the request object as Express in this case. This way of getting the route name is actually more stable as the previous implementation didn't work if the request never made it to the actual route handler (e.g. if an error occurred in a middleware handler). In that case the transaction would have been named `use` (the function name used to register the middleware handler). Fixes: #706
1 parent 3a4876b commit 0938282

File tree

6 files changed

+87
-55
lines changed

6 files changed

+87
-55
lines changed

.tav.yml

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,8 @@ cassandra-driver:
210210
commands: node test/instrumentation/modules/cassandra/cassandra.js
211211

212212
restify:
213-
# All versions before 4.4 are unsupported. Additionally,
214-
# there are several broken releases that need to be skipped:
215-
# - 6.2.0
216-
# - 6.0.1
217-
# - 6.0.0
218-
# - 5.0.0
219-
versions: '>=4.4 <5 || >5 <6 || >6.0.1 <6.2 || >6.2'
220-
commands:
221-
- node test/instrumentation/modules/restify.js
213+
versions: '>=5.2.0'
214+
commands: node test/instrumentation/modules/restify.js
222215

223216
finalhandler:
224217
versions: '*'

docs/supported-technologies.asciidoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ These are the frameworks that we officially support:
4949
|<<koa,Koa>> via koa-router |>=5.2.0 <8.0.0 |Koa doesn't have a built in router,
5050
so we can't support Koa directly since we rely on router information for full support.
5151
We currently support the most popular Koa router called https://github.com/alexmingoia/koa-router[koa-router]
52-
|<<restify,Restify>> |>=4.4 <8 |
52+
|<<restify,Restify>> |>=5.2.0 |
5353
|=======================================================================
5454

5555
[float]

lib/instrumentation/express-utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
var symbols = require('../symbols')
44

5+
// This function is also able to extract the path from a Restify request as
6+
// it's storing the route name on req.route.path as well
57
exports.getPathFromRequest = function (req) {
68
var path
79

lib/instrumentation/modules/restify.js

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,30 @@
11
'use strict'
22

3-
const isError = require('core-util-is').isError
3+
const semver = require('semver')
44

55
const shimmer = require('../shimmer')
66

7-
const httpMethods = [
8-
'del',
9-
'get',
10-
'head',
11-
'opts',
12-
'post',
13-
'put',
14-
'patch'
15-
]
16-
177
module.exports = function (restify, agent, version, enabled) {
188
if (!enabled) return restify
199
if (!agent._conf.frameworkName) agent._conf.frameworkName = 'restify'
2010
if (!agent._conf.frameworkVersion) agent._conf.frameworkVersion = version
2111

22-
const reportedSymbol = Symbol('reported')
23-
24-
function makeWrapHandler (name) {
25-
return function wrapHandler (handler) {
26-
return function wrappedHandler (request) {
27-
agent._instrumentation.setDefaultTransactionName(name)
28-
shimmer.wrap(arguments, 2, function wrapNext (next) {
29-
return function wrappedNext (err) {
30-
if (isError(err) && !err[reportedSymbol]) {
31-
err[reportedSymbol] = true
32-
agent.captureError(err, { request })
33-
}
34-
return next.apply(this, arguments)
35-
}
36-
})
37-
return handler.apply(this, arguments)
38-
}
39-
}
40-
}
41-
4212
function patchServer (server) {
43-
shimmer.massWrap(server, httpMethods, function wrapMethod (fn, method) {
44-
return function wrappedMethod (path) {
45-
const name = `${method.toUpperCase()} ${path}`
46-
const fns = Array.prototype.slice.call(arguments, 1).map(makeWrapHandler(name))
47-
return fn.apply(this, [path].concat(fns))
48-
}
49-
})
50-
51-
shimmer.massWrap(server, [ 'use', 'pre' ], function wrapMiddleware (fn, method) {
52-
return function wrappedMiddleware () {
53-
return fn.apply(this, Array.prototype.slice.call(arguments).map(makeWrapHandler(method)))
54-
}
55-
})
13+
if (semver.gte(version, '7.0.0')) {
14+
shimmer.wrap(server, '_onHandlerError', function (orig) {
15+
return function _wrappedOnHandlerError (err, req, res, isUncaught) {
16+
if (err) agent.captureError(err, { request: req, handled: !isUncaught })
17+
return orig.apply(this, arguments)
18+
}
19+
})
20+
} else {
21+
shimmer.wrap(server, '_emitErrorEvents', function (orig) {
22+
return function _wrappedOnHandlerError (req, res, route, err, cb) {
23+
if (err) agent.captureError(err, { request: req })
24+
return orig.apply(this, arguments)
25+
}
26+
})
27+
}
5628
}
5729

5830
shimmer.wrap(restify, 'createServer', function (fn) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
"standard": "^12.0.1",
139139
"tape": "^4.9.1",
140140
"tedious": "^2.6.1",
141-
"test-all-versions": "^3.3.3",
141+
"test-all-versions": "^4.0.0",
142142
"thunky": "^1.0.3",
143143
"untildify": "^3.0.3",
144144
"util.promisify": "^1.0.0",

test/instrumentation/modules/restify.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ test('transaction name', function (t) {
4141
// otherwise this will use IPv6, which fails on Travis CI.
4242
server.listen(null, '0.0.0.0', function () {
4343
const req = http.get(`${server.url}/hello/world`, res => {
44+
t.equal(res.statusCode, 200, 'server should respond with status code 200')
4445
const chunks = []
4546
res.on('data', chunks.push.bind(chunks))
4647
res.on('end', () => {
@@ -97,6 +98,7 @@ test('error reporting', function (t) {
9798
// otherwise this will use IPv6, which fails on Travis CI.
9899
server.listen(null, '0.0.0.0', function () {
99100
const req = http.get(`${server.url}/hello/world`, res => {
101+
t.equal(res.statusCode, 500, 'server should respond with status code 500')
100102
res.resume()
101103
res.on('end', () => {
102104
agent.flush()
@@ -149,6 +151,69 @@ test('error reporting from chained handler', function (t) {
149151
// otherwise this will use IPv6, which fails on Travis CI.
150152
server.listen(null, '0.0.0.0', function () {
151153
const req = http.get(`${server.url}/hello/world`, res => {
154+
t.equal(res.statusCode, 500, 'server should respond with status code 500')
155+
res.resume()
156+
res.on('end', () => {
157+
agent.flush()
158+
done()
159+
})
160+
})
161+
req.end()
162+
})
163+
})
164+
165+
test('error reporting from chained handler given as array', function (t) {
166+
resetAgent((data) => {
167+
t.ok(errored, 'reported an error')
168+
t.equal(data.transactions.length, 1, 'has a transaction')
169+
170+
const trans = data.transactions[0]
171+
t.equal(trans.name, 'GET /hello/:name', 'transaction name is GET /hello/:name')
172+
t.equal(trans.type, 'request', 'transaction type is request')
173+
t.end()
174+
})
175+
176+
let request
177+
let errored = false
178+
const error = new Error('wat')
179+
const captureError = agent.captureError
180+
agent.captureError = function (err, data) {
181+
t.equal(err, error, 'has the expected error')
182+
t.ok(data, 'captured data with error')
183+
t.equal(data.request, request, 'captured data has the request object')
184+
errored = true
185+
}
186+
t.on('end', function () {
187+
agent.captureError = captureError
188+
})
189+
190+
const server = restify.createServer()
191+
const done = once(() => {
192+
server.close()
193+
})
194+
t.on('end', done)
195+
196+
server.use([
197+
function (req, res, next) {
198+
next()
199+
},
200+
function (req, res, next) {
201+
request = req
202+
next(error)
203+
}
204+
])
205+
206+
// It's important to have the route registered. Otherwise the request will
207+
// not reach the middleware above and will just be ended with a 404
208+
server.get('/hello/:name', (req, res, next) => {
209+
t.fail('should never call route handler')
210+
})
211+
212+
// NOTE: Hostname must be supplied to force IPv4 mode,
213+
// otherwise this will use IPv6, which fails on Travis CI.
214+
server.listen(null, '0.0.0.0', function () {
215+
const req = http.get(`${server.url}/hello/world`, res => {
216+
t.equal(res.statusCode, 500, 'server should respond with status code 500')
152217
res.resume()
153218
res.on('end', () => {
154219
agent.flush()

0 commit comments

Comments
 (0)