Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proxy support for global fetch #696

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
3 changes: 2 additions & 1 deletion sdk_contrib/fetch/lib/fetch_p.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down Expand Up @@ -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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first idea was to pass down the entire args again but this would overwrite the changes we do to the request like adding the trace header as the headers from the options take priority over the headers that are already set for a request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this code:

const agent = new Agent({maxSockets: 1234});
const request = new Request('someurl', { dispatcher: agent });
fetch(request);

Here, fetch is only called with one parameter. The dispatcher is "baked in" into the request. For undici, it's actually a symbol property. When you call baseFetchFunction(requestClone, fetchOptions), requestClone doesn't contain that property, So that per-request-setting wouldn't be passed on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an attempt to fix this. Sorry for the complexity, but it should work more reliably for you.
#698

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#698 fixes a different issue that is there so I would open another issue to point to your PR to.


if (thisSubsegmentCallback) {
thisSubsegmentCallback(subsegment, requestClone, response);
Expand Down
46 changes: 46 additions & 0 deletions sdk_contrib/fetch/test/integration/fetch_p.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ let listener;
let server;
let goodUrl;
let receivedHeaders;
let proxyServer;
let proxyUrl;
let proxyListener;


before(() => {
Expand All @@ -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 () {
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions sdk_contrib/fetch/test/unit/fetch_p.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading