Skip to content

Commit 4ce8f77

Browse files
authored
Merge pull request #321 from willarmiros/emitters
replaced error listeners with errorMonitor
2 parents 62605b8 + e942c62 commit 4ce8f77

File tree

4 files changed

+62
-21
lines changed

4 files changed

+62
-21
lines changed

packages/core/lib/patchers/http_p.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var contextUtils = require('../context_utils');
1212
var Utils = require('../utils');
1313

1414
var logger = require('../logger');
15+
var events = require('events');
1516

1617
/**
1718
* Wraps the http/https.request() and .get() calls to automatically capture information for the segment.
@@ -138,7 +139,11 @@ function enableCapture(module, downstreamXRayEnabled, subsegmentCallback) {
138139
subsegment.close(e);
139140
}
140141

141-
if (this._events && this._events.error && this._events.error.length === 1) {
142+
// Only need to remove our listener & re-emit if we're not listening using the errorMonitor,
143+
// e.g. the app is running on Node 10. Otherwise the errorMonitor will re-emit automatically.
144+
// See: https://github.com/aws/aws-xray-sdk-node/issues/318
145+
// TODO: Remove this logic once node 12 support is deprecated
146+
if (!events.errorMonitor && this.listenerCount('error') <= 1) {
142147
this.removeListener('error', errorCapturer);
143148
this.emit('error', e);
144149
}
@@ -174,12 +179,17 @@ function enableCapture(module, downstreamXRayEnabled, subsegmentCallback) {
174179
} else {
175180
callback(res);
176181
}
177-
// if no callback provided and there is only SDK added response listener,
178-
// we consume the response so the actual end can fire.
179-
} else if (res && res.listenerCount('end') === 1) {
182+
// if no callback provided by user application, AND no explicit response listener
183+
// added by user application, then we consume the response so the 'end' event fires
184+
// See: https://nodejs.org/api/http.html#http_class_http_clientrequest
185+
} else if (res.req && res.req.listenerCount('response') === 0) {
180186
res.resume();
181187
}
182-
}).on('error', errorCapturer);
188+
});
189+
190+
// Use errorMonitor if available (in Node 12.17+), otherwise fall back to standard error listener
191+
// See: https://nodejs.org/dist/latest-v12.x/docs/api/events.html#events_eventemitter_errormonitor
192+
req.on(events.errorMonitor || 'error', errorCapturer);
183193

184194
return req;
185195
}

packages/core/test/unit/patchers/http_p.test.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var chai = require('chai');
44
var sinon = require('sinon');
55
var sinonChai = require('sinon-chai');
66
var URL = require('url');
7+
var events = require('events');
78

89
var captureHTTPs = require('../../../lib/patchers/http_p').captureHTTPs;
910
var captureHTTPsGlobal = require('../../../lib/patchers/http_p').captureHTTPsGlobal;
@@ -128,14 +129,15 @@ describe('HTTP/S', function() {
128129
});
129130

130131
describe('on invocation', function() {
131-
var capturedHttp, fakeRequest, fakeResponse, httpClient, requestSpy, sandbox;
132+
var capturedHttp, fakeRequest, fakeResponse, httpClient, requestSpy, resumeSpy, sandbox;
132133

133134
beforeEach(function() {
134135
sandbox = sinon.sandbox.create();
135136
segment = new Segment('test', traceId);
136137

137138
fakeRequest = buildFakeRequest();
138139
fakeResponse = buildFakeResponse();
140+
fakeResponse.req = fakeRequest;
139141

140142
httpClient = { request: function(...args) {
141143
const callback = args[typeof args[1] === 'object' ? 2 : 1];
@@ -144,8 +146,10 @@ describe('HTTP/S', function() {
144146
}};
145147
httpClient.get = httpClient.request;
146148

149+
resumeSpy = sandbox.spy(fakeResponse, 'resume');
147150
requestSpy = sandbox.spy(httpClient, 'request');
148-
capturedHttp = captureHTTPs(httpClient, true); });
151+
capturedHttp = captureHTTPs(httpClient, true);
152+
});
149153

150154
afterEach(function() {
151155
sandbox.restore();
@@ -158,6 +162,22 @@ describe('HTTP/S', function() {
158162
resolveManualStub.should.have.been.calledWith(options);
159163
});
160164

165+
it('should consume the response if no callback is provided by user', function() {
166+
capturedHttp.request(httpOptions); // no callback
167+
resumeSpy.should.have.been.calledOnce;
168+
});
169+
170+
it('should not consume the response if a callback is provided by user', function() {
171+
capturedHttp.request(httpOptions, () => {});
172+
resumeSpy.should.not.have.been.called;
173+
});
174+
175+
it('should not consume the response if a response listener is provided by user', function() {
176+
fakeRequest.on('response', () => {});
177+
capturedHttp.request(httpOptions);
178+
resumeSpy.should.not.have.been.called;
179+
});
180+
161181
it('should create a new subsegment with name as hostname', function() {
162182
var options = {hostname: 'hostname', path: '/'};
163183
capturedHttp.request(options);
@@ -206,16 +226,14 @@ describe('HTTP/S', function() {
206226
assert.match(options.headers['X-Amzn-Trace-Id'], xAmznTraceId);
207227
});
208228

209-
if (process.version.startsWith('v') && process.version >= 'v10') {
210-
it('should inject the tracing headers into the options if a URL is also provided', function() {
211-
capturedHttp.request(`http://${httpOptions.host}${httpOptions.path}`, httpOptions);
212-
213-
// example: 'Root=1-59138384-82ff54d5ba9282f0c680adb3;Parent=53af362e4e4efeb8;Sampled=1'
214-
var xAmznTraceId = new RegExp('^Root=' + traceId + ';Parent=([a-f0-9]{16});Sampled=1$');
215-
var options = requestSpy.firstCall.args[1];
216-
assert.match(options.headers['X-Amzn-Trace-Id'], xAmznTraceId);
217-
});
218-
}
229+
it('should inject the tracing headers into the options if a URL is also provided', function() {
230+
capturedHttp.request(`http://${httpOptions.host}${httpOptions.path}`, httpOptions);
231+
232+
// example: 'Root=1-59138384-82ff54d5ba9282f0c680adb3;Parent=53af362e4e4efeb8;Sampled=1'
233+
var xAmznTraceId = new RegExp('^Root=' + traceId + ';Parent=([a-f0-9]{16});Sampled=1$');
234+
var options = requestSpy.firstCall.args[1];
235+
assert.match(options.headers['X-Amzn-Trace-Id'], xAmznTraceId);
236+
});
219237

220238
it('should return the request object', function() {
221239
var request = capturedHttp.request(httpOptions);
@@ -385,6 +403,15 @@ describe('HTTP/S', function() {
385403
done();
386404
}, 50);
387405
});
406+
407+
if (process.version.startsWith('v') && process.version >= 'v12.17') {
408+
it('should still re-emit if there are multiple errorMonitors attached', function() {
409+
fakeRequest.on(events.errorMonitor, function() {});
410+
fakeRequest.on(events.errorMonitor, function() {});
411+
412+
assert.throws(function() { fakeRequest.emitter.emit('error', error); });
413+
});
414+
}
388415
});
389416
});
390417
});

packages/mysql/lib/mysql_p.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
var AWSXRay = require('aws-xray-sdk-core');
6+
var events = require('events');
67
var SqlData = AWSXRay.database.SqlData;
78

89
var DATABASE_VERS = process.env.MYSQL_DATABASE_VERSION;
@@ -214,13 +215,14 @@ function captureOperation(name) {
214215
var errorCapturer = function (err) {
215216
subsegment.close(err);
216217

217-
if (this._events && this._events.error && this._events.error.length === 1) {
218+
// TODO: Remove this logic once Node 10 is deprecated
219+
if (!events.errorMonitor && this.listenerCount('error') <= 1) {
218220
this.removeListener('error', errorCapturer);
219221
this.emit('error', err);
220222
}
221223
};
222224

223-
command.on('error', errorCapturer);
225+
command.on(events.errorMonitor || 'error', errorCapturer);
224226
}
225227

226228
subsegment.addSqlData(createSqlData(config, command));

packages/postgres/lib/postgres_p.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
var AWSXRay = require('aws-xray-sdk-core');
6+
var events = require('events');
67
var SqlData = AWSXRay.database.SqlData;
78

89
var DATABASE_VERS = process.env.POSTGRES_DATABASE_VERSION;
@@ -106,13 +107,14 @@ function captureQuery() {
106107
var errorCapturer = function (err) {
107108
subsegment.close(err);
108109

109-
if (this._events && this._events.error && this._events.error.length === 1) {
110+
// TODO: Remove this logic once Node 10 is deprecated
111+
if (!events.errorMonitor && this.listenerCount('error') <= 1) {
110112
this.removeListener('error', errorCapturer);
111113
this.emit('error', err);
112114
}
113115
};
114116

115-
query.on('error', errorCapturer);
117+
query.on(events.errorMonitor || 'error', errorCapturer);
116118
}
117119

118120
subsegment.addSqlData(createSqlData(this.connectionParameters, query));

0 commit comments

Comments
 (0)