diff --git a/sdk_contrib/fetch/lib/fetch_p.js b/sdk_contrib/fetch/lib/fetch_p.js index dd7c4f2c..4686078c 100644 --- a/sdk_contrib/fetch/lib/fetch_p.js +++ b/sdk_contrib/fetch/lib/fetch_p.js @@ -54,6 +54,27 @@ function captureFetchModule(module, downstreamXRayEnabled, subsegmentCallback) { return module.default; } +function transferDispatcherNodeJSBuggyUndici(request, requestClone) { + const dispatcherSymbol = Object.getOwnPropertySymbols(request).find(symbol => symbol.description === 'dispatcher'); + if (dispatcherSymbol) { + requestClone[dispatcherSymbol] = request[dispatcherSymbol]; + } +} + +function getTransferDispatcherNodeJSBuggyUndici(requestClass) { + try { + const { Agent } = require('http'); + const request = new requestClass('http://example.org', { dispatcher: new Agent }); + const requestClone = request.clone(); + const dispatcherSymbol = Object.getOwnPropertySymbols(request).find(symbol => symbol.description === 'dispatcher'); + if (request[dispatcherSymbol] && !requestClone[dispatcherSymbol]) { + return transferDispatcherNodeJSBuggyUndici; + } + } catch { + } + return function() {}; +} + /** * Return a fetch function that will pass segment information to the target host. * This does not change any globals @@ -66,12 +87,12 @@ function captureFetchModule(module, downstreamXRayEnabled, subsegmentCallback) { * @returns Response */ function enableCapture(baseFetchFunction, requestClass, downstreamXRayEnabled, subsegmentCallback) { - + const transferDispatcher = getTransferDispatcherNodeJSBuggyUndici(requestClass); const overridenFetchAsync = async (...args) => { const thisDownstreamXRayEnabled = !!downstreamXRayEnabled; const thisSubsegmentCallback = subsegmentCallback; // Standardize request information - const request = typeof args[0] === 'object' ? + const request = (args[0] instanceof requestClass && args.length === 1) ? args[0] : new requestClass(...args); @@ -125,6 +146,9 @@ function enableCapture(baseFetchFunction, requestClass, downstreamXRayEnabled, s // Set up fetch call and capture any thrown errors const capturedFetch = async () => { const requestClone = request.clone(); + + transferDispatcher(request, requestClone); + let response; try { response = await baseFetchFunction(requestClone); diff --git a/sdk_contrib/fetch/test/integration/fetch_p.test.js b/sdk_contrib/fetch/test/integration/fetch_p.test.js index 3d633e38..25ffccc2 100644 --- a/sdk_contrib/fetch/test/integration/fetch_p.test.js +++ b/sdk_contrib/fetch/test/integration/fetch_p.test.js @@ -111,6 +111,26 @@ describe('Integration tests', function () { stubClose.should.have.been.calledOnce; }); + it('works with stringifyable objects', async function () { + const spyCallback = sandbox.spy(); + const fetch = captureFetchGlobal(true, spyCallback); + const response = await fetch(new URL(goodUrl), { + headers: { + 'foo': 'bar' + } + }); + response.status.should.equal(200); + receivedHeaders.should.to.have.property('x-amzn-trace-id'); + receivedHeaders.should.to.have.property('foo', 'bar'); + (await response.text()).should.contain('Example'); + stubIsAutomaticMode.should.have.been.called; + stubAddNewSubsegment.should.have.been.calledOnce; + stubResolveSegment.should.have.been.calledOnce; + stubAddFetchRequestData.should.have.been.calledOnce; + stubAddErrorFlag.should.not.have.been.calledOnce; + stubClose.should.have.been.calledOnce; + }); + it('sets error flag on failed fetch when global fetch exists', async function () { const spyCallback = sandbox.spy(); const fetch = captureFetchGlobal(true, spyCallback); diff --git a/sdk_contrib/fetch/test/unit/fetch_p.test.js b/sdk_contrib/fetch/test/unit/fetch_p.test.js index 4dde8d52..d8b74912 100644 --- a/sdk_contrib/fetch/test/unit/fetch_p.test.js +++ b/sdk_contrib/fetch/test/unit/fetch_p.test.js @@ -1,4 +1,5 @@ const { Subsegment } = require('aws-xray-sdk-core'); +const { Agent } = require('http'); const fetch = require('node-fetch'); describe('Unit tests', function () { @@ -157,16 +158,79 @@ describe('Unit tests', function () { it('short circuits if headers include trace ID', async function () { const activeFetch = captureFetch(true); - const request = new fetchModule.Request('https://www.foo.com', { + const request = new FetchRequest('https://www.foo.com', { headers: { 'X-Amzn-Trace-Id': '12345' } }); await activeFetch(request); - stubFetch.should.have.been.calledOnceWith(request); + stubFetch.should.have.been.calledOnceWith(sinon.match({ url: 'https://www.foo.com/'})); stubResolveSegment.should.not.have.been.called; }); + it('transfers dispatcher property for undici', async function () { + const activeFetch = captureFetch(true); + const agent = new Agent({ + maxSockets: 1234 + }); + await activeFetch('https://www.foo.com', { + dispatcher: agent + }); + stubFetch.should.have.been.calledOnceWith(sinon.match({ url: 'https://www.foo.com/' })); + stubResolveSegment.should.have.been.called; + + // check if dispatcher was transferred + const dummyRequest = new FetchRequest('https://www.foo.com', { + dispatcher: agent + }); + const dispatcherSymbol = Object.getOwnPropertySymbols(dummyRequest).find(symbol => symbol.description === 'dispatcher'); + if (dispatcherSymbol) { + stubFetch.should.have.been.calledOnceWith(sinon.match({ [dispatcherSymbol]: agent })); + } + }); + + it('transfers dispatcher property for undici with Request object', async function () { + const activeFetch = captureFetch(true); + const agent = new Agent({ + maxSockets: 1234 + }); + await activeFetch(new FetchRequest('https://www.foo.com'), { + dispatcher: agent + }); + stubFetch.should.have.been.calledOnceWith(sinon.match({ url: 'https://www.foo.com/' })); + stubResolveSegment.should.have.been.called; + + // check if dispatcher was transferred + const dummyRequest = new FetchRequest('https://www.foo.com', { + dispatcher: agent + }); + const dispatcherSymbol = Object.getOwnPropertySymbols(dummyRequest).find(symbol => symbol.description === 'dispatcher'); + if (dispatcherSymbol) { + stubFetch.should.have.been.calledOnceWith(sinon.match({ [dispatcherSymbol]: agent })); + } + }); + + it('transfers dispatcher property for undici with url and init', async function () { + const activeFetch = captureFetch(true); + const agent = new Agent({ + maxSockets: 1234 + }); + await activeFetch('https://www.foo.com', { + dispatcher: agent + }); + stubFetch.should.have.been.calledOnceWith(sinon.match({ url: 'https://www.foo.com/' })); + stubResolveSegment.should.have.been.called; + + // check if dispatcher was transferred + const dummyRequest = new FetchRequest('https://www.foo.com', { + dispatcher: agent + }); + const dispatcherSymbol = Object.getOwnPropertySymbols(dummyRequest).find(symbol => symbol.description === 'dispatcher'); + if (dispatcherSymbol) { + stubFetch.should.have.been.calledOnceWith(sinon.match({ [dispatcherSymbol]: agent })); + } + }); + it('calls base function when no parent and automatic mode', async function () { const activeFetch = captureFetch(true); stubResolveSegment.returns(null);