diff --git a/package-lock.json b/package-lock.json index 5142b3cd..f5fea8e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "sinon-chai": "^3.5.0", "tsd": "^0.25.0", "typescript": "^4.4.4", + "undici": "^6.21.0", "upath": "^1.2.0" }, "engines": { @@ -18701,6 +18702,16 @@ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", "dev": true }, + "node_modules/undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -24195,6 +24206,7 @@ "test-aws-xray-sdk-express": "file:packages/test_express", "tsd": "^0.25.0", "typescript": "^4.4.4", + "undici": "^6.21.0", "upath": "^1.2.0" }, "dependencies": { @@ -38793,6 +38805,12 @@ } } }, + "undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "dev": true + }, "unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -49610,6 +49628,12 @@ } } }, + "undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "dev": true + }, "unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", diff --git a/package.json b/package.json index 7515a838..89474dc6 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "sinon-chai": "^3.5.0", "tsd": "^0.25.0", "typescript": "^4.4.4", + "undici": "^6.21.0", "upath": "^1.2.0" }, "engines": { diff --git a/sdk_contrib/fetch/lib/fetch_p.js b/sdk_contrib/fetch/lib/fetch_p.js index dd7c4f2c..454720bf 100644 --- a/sdk_contrib/fetch/lib/fetch_p.js +++ b/sdk_contrib/fetch/lib/fetch_p.js @@ -77,6 +77,7 @@ function enableCapture(baseFetchFunction, requestClass, downstreamXRayEnabled, s // Facilitate the addition of Segment information via the request arguments const params = args.length > 1 ? args[1] : {}; + const fetchOptions = 'dispatcher' in params ? {dispatcher: params.dispatcher} : undefined; // Short circuit if the HTTP is already being captured if (request.headers.has('X-Amzn-Trace-Id')) { @@ -127,7 +128,7 @@ function enableCapture(baseFetchFunction, requestClass, downstreamXRayEnabled, s const requestClone = request.clone(); let response; try { - response = await baseFetchFunction(requestClone); + response = await baseFetchFunction(requestClone, fetchOptions); if (thisSubsegmentCallback) { thisSubsegmentCallback(subsegment, requestClone, response); diff --git a/sdk_contrib/fetch/test/integration/fetch_p.test.js b/sdk_contrib/fetch/test/integration/fetch_p.test.js index 3d633e38..67b5390d 100644 --- a/sdk_contrib/fetch/test/integration/fetch_p.test.js +++ b/sdk_contrib/fetch/test/integration/fetch_p.test.js @@ -3,6 +3,9 @@ let listener; let server; let goodUrl; let receivedHeaders; +let proxyServer; +let proxyUrl; +let proxyListener; before(() => { @@ -19,11 +22,37 @@ before(() => { const address = server.address(); const host = address.family === 'IPv6' ? `[${address.address}]` : address.address; goodUrl = `http://${host}:${address.port}/test`; + + proxyServer = http.createServer(); + proxyServer.on('connect', (req, socket) => { + const res = new http.ServerResponse(req); + res.assignSocket(socket); + + const lastColon = req.url.lastIndexOf(':'); + const host = req.url.substring(0, lastColon); + const port = parseInt(req.url.substring(lastColon + 1), 10); + const opts = {host: host.replace(/^\[|\]$/g, ''), port}; + + const net = require('net'); + const target = net.connect(opts, () => { + res.writeHead(200); + res.flushHeaders(); + res.detachSocket(socket); + + socket.pipe(target); + target.pipe(socket); + }); + }); + proxyListener = proxyServer.listen(); + const proxyAddress = proxyServer.address(); + const proxyHost = proxyAddress.family === 'IPv6' ? `[${proxyAddress.address}]` : proxyAddress.address; + proxyUrl = `http://${proxyHost}:${proxyAddress.port}`; }); after(() => { // close http server listener.close(); + proxyListener.close(); }); describe('Integration tests', function () { @@ -111,6 +140,23 @@ describe('Integration tests', function () { stubClose.should.have.been.calledOnce; }); + it('adds headers when called with fetchOptions', async function () { + const spyCallback = sandbox.spy(); + const fetch = captureFetchGlobal(true, spyCallback); + const undici = require('undici'); + const response = await fetch(goodUrl, {dispatcher: new undici.ProxyAgent(proxyUrl), 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..2683a56c 100644 --- a/sdk_contrib/fetch/test/unit/fetch_p.test.js +++ b/sdk_contrib/fetch/test/unit/fetch_p.test.js @@ -339,6 +339,20 @@ describe('Unit tests', function () { response.should.equal(stubValidResponse); }); + it('resolves to response through proxy when fetch options are supplied', async function () { + const activeFetch = captureFetch(true); + const proxyStub = sinon.stub(); + const request = new FetchRequest('https://www.foo.com/test'); + const response = await activeFetch(request, { + dispatcher: proxyStub + }); + stubFetch.should.have.been.calledOnce; + const callArgs = stubFetch.firstCall.args; + callArgs[0].should.contain(request); + callArgs[1].dispatcher.should.equal(proxyStub); + response.should.equal(stubValidResponse); + }); + it('calls subsegmentCallback with error upon fetch throwing', async function () { const spyCallback = sandbox.spy(); const activeFetch = captureFetch(true, spyCallback);