feat: you can now supply your own HTTP agent to a web3.js Connection (#29125)

* You can now supply your own `https?.Agent` when creating a `Connection` object

* Don't use HTTP agents in test mode

* Tests that assert the behaviour of the `agentOverride` config of `Connection`

* s/agentOverride/httpAgent/ is less confusing when the value is `false`
This commit is contained in:
Steven Luscher 2022-12-06 14:57:13 -08:00 committed by GitHub
parent aeb6b53502
commit f1427dd90c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 5 deletions

View File

@ -51,7 +51,7 @@
"pretty": "prettier --check '{,{src,test}/**/}*.{j,t}s'",
"pretty:fix": "prettier --write '{,{src,test}/**/}*.{j,t}s'",
"re": "semantic-release --repository-url git@github.com:solana-labs/solana-web3.js.git",
"test": "cross-env TS_NODE_COMPILER_OPTIONS='{ \"module\": \"commonjs\", \"target\": \"es2019\" }' ts-mocha --require esm './test/**/*.test.ts'",
"test": "cross-env NODE_ENV=test TS_NODE_COMPILER_OPTIONS='{ \"module\": \"commonjs\", \"target\": \"es2019\" }' ts-mocha --require esm './test/**/*.test.ts'",
"test:cover": "nyc --reporter=lcov npm run test",
"test:live": "TEST_LIVE=1 npm run test",
"test:live-with-test-validator": "start-server-and-test 'solana-test-validator --reset --quiet' http://localhost:8899/health test:live"

View File

@ -2,6 +2,8 @@ import bs58 from 'bs58';
import {Buffer} from 'buffer';
// @ts-ignore
import fastStableStringify from 'fast-stable-stringify';
import type {Agent as HttpAgent} from 'http';
import {Agent as HttpsAgent} from 'https';
import {
type as pick,
number,
@ -1450,11 +1452,47 @@ function createRpcClient(
customFetch?: FetchFn,
fetchMiddleware?: FetchMiddleware,
disableRetryOnRateLimit?: boolean,
httpAgent?: HttpAgent | HttpsAgent | false,
): RpcClient {
const fetch = customFetch ? customFetch : fetchImpl;
let agentManager: AgentManager | undefined;
if (!process.env.BROWSER) {
agentManager = new AgentManager(url.startsWith('https:') /* useHttps */);
let agentManager:
| {requestEnd(): void; requestStart(): HttpAgent | HttpsAgent}
| undefined;
if (process.env.BROWSER) {
if (httpAgent != null) {
console.warn(
'You have supplied an `httpAgent` when creating a `Connection` in a browser environment.' +
'It has been ignored; `httpAgent` is only used in Node environments.',
);
}
} else {
if (httpAgent == null) {
if (process.env.NODE_ENV !== 'test') {
agentManager = new AgentManager(
url.startsWith('https:') /* useHttps */,
);
}
} else {
if (httpAgent !== false) {
const isHttps = url.startsWith('https:');
if (isHttps && !(httpAgent instanceof HttpsAgent)) {
throw new Error(
'The endpoint `' +
url +
'` can only be paired with an `https.Agent`. You have, instead, supplied an ' +
'`http.Agent` through `httpAgent`.',
);
} else if (!isHttps && httpAgent instanceof HttpsAgent) {
throw new Error(
'The endpoint `' +
url +
'` can only be paired with an `http.Agent`. You have, instead, supplied an ' +
'`https.Agent` through `httpAgent`.',
);
}
agentManager = {requestEnd() {}, requestStart: () => httpAgent};
}
}
}
let fetchWithMiddleware: FetchFn | undefined;
@ -2855,6 +2893,12 @@ export type FetchMiddleware = (
* Configuration for instantiating a Connection
*/
export type ConnectionConfig = {
/**
* An `http.Agent` that will be used to manage socket connections (eg. to implement connection
* persistence). Set this to `false` to create a connection that uses no agent. This applies to
* Node environments only.
*/
httpAgent?: HttpAgent | HttpsAgent | false;
/** Optional commitment level */
commitment?: Commitment;
/** Optional endpoint URL to the fullnode JSON RPC PubSub WebSocket Endpoint */
@ -2972,6 +3016,7 @@ export class Connection {
let fetch;
let fetchMiddleware;
let disableRetryOnRateLimit;
let httpAgent;
if (commitmentOrConfig && typeof commitmentOrConfig === 'string') {
this._commitment = commitmentOrConfig;
} else if (commitmentOrConfig) {
@ -2983,6 +3028,7 @@ export class Connection {
fetch = commitmentOrConfig.fetch;
fetchMiddleware = commitmentOrConfig.fetchMiddleware;
disableRetryOnRateLimit = commitmentOrConfig.disableRetryOnRateLimit;
httpAgent = commitmentOrConfig.httpAgent;
}
this._rpcEndpoint = assertEndpointUrl(endpoint);
@ -2994,6 +3040,7 @@ export class Connection {
fetch,
fetchMiddleware,
disableRetryOnRateLimit,
httpAgent,
);
this._rpcRequest = createRpcRequest(this._rpcClient);
this._rpcBatchRequest = createRpcBatchRequest(this._rpcClient);

View File

@ -3,8 +3,10 @@ import {Buffer} from 'buffer';
import * as splToken from '@solana/spl-token';
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {Agent as HttpAgent} from 'http';
import {Agent as HttpsAgent} from 'https';
import {AbortController} from 'node-abort-controller';
import {mock, useFakeTimers, SinonFakeTimers} from 'sinon';
import {match, mock, spy, useFakeTimers, SinonFakeTimers} from 'sinon';
import sinonChai from 'sinon-chai';
import {
@ -188,6 +190,50 @@ describe('Connection', function () {
});
}
describe('override HTTP agent', () => {
let previousBrowserEnv;
beforeEach(() => {
previousBrowserEnv = process.env.BROWSER;
delete process.env.BROWSER;
});
afterEach(() => {
process.env.BROWSER = previousBrowserEnv;
});
it('uses no agent with fetch when `overrideAgent` is `false`', () => {
const fetch = spy();
const c = new Connection(url, {httpAgent: false, fetch});
c.getBlock(0);
expect(fetch).to.have.been.calledWith(
match.any,
match({agent: undefined}),
);
});
it('uses the supplied `overrideAgent` with fetch', () => {
const fetch = spy();
const httpAgent = new HttpsAgent();
const c = new Connection('https://example.com', {httpAgent, fetch});
c.getBlock(0);
expect(fetch).to.have.been.calledWith(
match.any,
match({agent: httpAgent}),
);
});
it('throws when the supplied `overrideAgent` is http but the endpoint is https', () => {
expect(() => {
new Connection('https://example.com', {httpAgent: new HttpAgent()});
}).to.throw;
});
it('throws when the supplied `overrideAgent` is https but the endpoint is http', () => {
expect(() => {
new Connection('http://example.com', {httpAgent: new HttpsAgent()});
}).to.throw;
});
});
it('should attribute middleware fatals to the middleware', async () => {
let connection = new Connection(url, {
// eslint-disable-next-line @typescript-eslint/no-unused-vars