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

feat: enable proxy handling for native fetch and connectrpc calls #1124

Merged
6 changes: 5 additions & 1 deletion cli/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
COSMO_API_KEY=cosmo_669b576aaadc10ee1ae81d9193425705
COSMO_API_URL=http://localhost:3001
CDN_URL=http://localhost:11000
CDN_URL=http://localhost:11000

# configure running wgc behind a proxy
# HTTPS_PROXY=""
# HTTP_PROXY=""
4 changes: 3 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"dotenv": "^16.3.1",
"env-paths": "^3.0.0",
"graphql": "^16.9.0",
"https-proxy-agent": "^7.0.5",
"inquirer": "^9.2.7",
"js-yaml": "^4.1.0",
"jwt-decode": "^3.1.2",
Expand All @@ -59,7 +60,8 @@
"open": "^9.1.0",
"ora": "^8.0.1",
"pathe": "^1.1.1",
"picocolors": "^1.0.0"
"picocolors": "^1.0.0",
"undici": "^6.19.8"
},
"devDependencies": {
"@types/cli-progress": "^3.11.5",
Expand Down
14 changes: 14 additions & 0 deletions cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,23 @@ import ContractCommands from './contract/index.js';
import FeatureGraphCommands from './feature-subgraph/index.js';
import FeatureFlagCommands from './feature-flag/index.js';

const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;

if (proxyUrl) {
// Lazy load undici only when needed
const { setGlobalDispatcher, ProxyAgent } = await import('undici');

// Set the global dispatcher for undici to route through the proxy
const dispatcher = new ProxyAgent({
uri: new URL(proxyUrl).toString(),
});
setGlobalDispatcher(dispatcher);
}

const client = CreateClient({
baseUrl: config.baseURL,
apiKey: config.apiKey,
proxyUrl,
});

const program = new Command();
Expand Down
6 changes: 5 additions & 1 deletion cli/src/core/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { compressionBrotli, compressionGzip, createConnectTransport } from '@con
import { createPromiseClient, PromiseClient } from '@connectrpc/connect';
import { PlatformService } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_connect';
import { NodeService } from '@wundergraph/cosmo-connect/dist/node/v1/node_connect';
import { HttpsProxyAgent } from 'https-proxy-agent';

export interface ClientOptions {
baseUrl: string;
apiKey?: string;
proxyUrl?: string;
}

export interface Client {
Expand All @@ -20,7 +22,9 @@ export const CreateClient = (opts: ClientOptions): Client => {

// You have to tell the Node.js http API which HTTP version to use.
httpVersion: '1.1',

nodeOptions: {
...(opts.proxyUrl ? { agent: new HttpsProxyAgent(opts.proxyUrl) } : {}),
},
// Avoid compression for small requests
compressMinBytes: 1024,

Expand Down
103 changes: 103 additions & 0 deletions cli/test/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { setGlobalDispatcher, ProxyAgent } from 'undici';
import { HttpsProxyAgent } from 'https-proxy-agent';

// Mocks the actual used undici modules
// The tests check if those have been configured
// correctly after starting the cli with or without
// a proxy
vi.mock('undici', () => ({
setGlobalDispatcher: vi.fn(),
ProxyAgent: vi.fn().mockImplementation((opts) => ({
uri: opts.uri,
})),
}));

// Mocks https-proxy-agent used inside the
// connectrpc platform client which will be
// injected whenever a proxy was configured
vi.mock('https-proxy-agent', () => ({
HttpsProxyAgent: vi.fn().mockImplementation((proxyUrl) => ({
proxyUrl,
})),
}));

// ensure all env variables and spies are cleaned up after each test
const prepare = () => {
// resets e.g. ProxyAgent mock state between describes
vi.resetModules();
vi.resetAllMocks();

delete process.env.HTTPS_PROXY;
delete process.env.HTTP_PROXY;
};

beforeEach(prepare);

describe('that when the HTTP_PROXY variable is set', () => {
test('the ProxyAgent for native fetch is configured using HTTP_PROXY', async () => {
process.env.HTTP_PROXY = 'http://proxy-server:8080';
await import('../src/commands/index.js');

expect(ProxyAgent).toHaveBeenCalledOnce();
expect(ProxyAgent).toHaveBeenCalledWith({
uri: 'http://proxy-server:8080/',
});

expect(setGlobalDispatcher).toHaveBeenCalledOnce();
});
});

describe('that when the HTTPS_PROXY variable is set', () => {
test('the fetch ProxyAgent is set without a trailing slash in HTTPS_PROXY', async () => {
process.env.HTTPS_PROXY = 'https://proxy-server:8080';

await import('../src/commands/index.js');

expect(ProxyAgent).toHaveBeenCalledOnce();
expect(ProxyAgent).toHaveBeenCalledWith({
uri: 'https://proxy-server:8080/',
});

expect(setGlobalDispatcher).toHaveBeenCalledOnce();
});
test('the fetch ProxyAgent is set with a trailing slash in HTTPS_PROXY', async () => {
process.env.HTTPS_PROXY = 'https://proxy-server:8080/';

await import('../src/commands/index.js');

expect(ProxyAgent).toHaveBeenCalledOnce();
expect(ProxyAgent).toHaveBeenCalledWith({
uri: 'https://proxy-server:8080/',
});

expect(setGlobalDispatcher).toHaveBeenCalledOnce();
});
});

describe('Platform Client Proxy Configuration', () => {
test('that a proxy agent is not created when no proxyUrl is provided', async () => {
await import('../src/commands/index.js');
expect(HttpsProxyAgent).not.toHaveBeenCalled();
});

test('that a proxy agent is created when a proxyUrl is provided', async () => {
process.env.HTTPS_PROXY = 'https://proxy-server:8080';

await import('../src/commands/index.js');
expect(HttpsProxyAgent).toHaveBeenCalledOnce();
});
});

describe('when the HTTPS_PROXY variable is not set', () => {
test('the global undici or connect proxy is not initizalized', async () => {
await import('../src/commands/index.js');

// native fetch
expect(ProxyAgent).not.toHaveBeenCalled();
expect(setGlobalDispatcher).not.toHaveBeenCalled();

// connect client
expect(HttpsProxyAgent).not.toHaveBeenCalled();
});
});
Loading
Loading