From c44812fa715a4bda78a07a38276023c40d353065 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 26 Apr 2021 08:35:07 -0700 Subject: [PATCH] feat: introduce support for custom HTTP headers (#16599) * feat: introduce support for custom http headers * feat: add fetch middleware --- web3.js/src/connection.ts | 92 +++++++++++++++++++++++++++++---- web3.js/test/connection.test.ts | 43 +++++++++++++++ web3.js/test/mocks/rpc-http.ts | 5 +- 3 files changed, 130 insertions(+), 10 deletions(-) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index efee786de..01271ff13 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -622,21 +622,45 @@ export type PerfSample = { samplePeriodSecs: number; }; -function createRpcClient(url: string, useHttps: boolean): RpcClient { +function createRpcClient( + url: string, + useHttps: boolean, + httpHeaders?: HttpHeaders, + fetchMiddleware?: FetchMiddleware, +): RpcClient { let agentManager: AgentManager | undefined; if (!process.env.BROWSER) { agentManager = new AgentManager(useHttps); } + let fetchWithMiddleware: (url: string, options: any) => Promise; + + if (fetchMiddleware) { + fetchWithMiddleware = (url: string, options: any) => { + return new Promise((resolve, reject) => { + fetchMiddleware(url, options, async (url: string, options: any) => { + try { + resolve(await fetch(url, options)); + } catch (error) { + reject(error); + } + }); + }); + }; + } + const clientBrowser = new RpcClient(async (request, callback) => { const agent = agentManager ? agentManager.requestStart() : undefined; const options = { method: 'POST', body: request, agent, - headers: { - 'Content-Type': 'application/json', - }, + headers: Object.assign( + { + 'Content-Type': 'application/json', + }, + httpHeaders || {}, + ), }; try { @@ -644,7 +668,12 @@ function createRpcClient(url: string, useHttps: boolean): RpcClient { let res: Response; let waitTime = 500; for (;;) { - res = await fetch(url, options); + if (fetchWithMiddleware) { + res = await fetchWithMiddleware(url, options); + } else { + res = await fetch(url, options); + } + if (res.status !== 429 /* Too many requests */) { break; } @@ -1673,6 +1702,32 @@ export type ConfirmedSignatureInfo = { blockTime?: number | null; }; +/** + * An object defining headers to be passed to the RPC server + */ +export type HttpHeaders = {[header: string]: string}; + +/** + * A callback used to augment the outgoing HTTP request + */ +export type FetchMiddleware = ( + url: string, + options: any, + fetch: Function, +) => void; + +/** + * Configuration for instantiating a Connection + */ +export type ConnectionConfig = { + /** Optional commitment level */ + commitment?: Commitment; + /** Optional HTTP headers object */ + httpHeaders?: HttpHeaders; + /** Optional fetch middleware callback */ + fetchMiddleware?: FetchMiddleware; +}; + /** * A connection to a fullnode JSON RPC endpoint */ @@ -1734,18 +1789,36 @@ export class Connection { * Establish a JSON RPC connection * * @param endpoint URL to the fullnode JSON RPC endpoint - * @param commitment optional default commitment level + * @param commitmentOrConfig optional default commitment level or optional ConnectionConfig configuration object */ - constructor(endpoint: string, commitment?: Commitment) { + constructor( + endpoint: string, + commitmentOrConfig?: Commitment | ConnectionConfig, + ) { this._rpcEndpoint = endpoint; let url = urlParse(endpoint); const useHttps = url.protocol === 'https:'; - this._rpcClient = createRpcClient(url.href, useHttps); + let httpHeaders; + let fetchMiddleware; + if (commitmentOrConfig && typeof commitmentOrConfig === 'string') { + this._commitment = commitmentOrConfig; + } else if (commitmentOrConfig) { + this._commitment = commitmentOrConfig.commitment; + httpHeaders = commitmentOrConfig.httpHeaders; + fetchMiddleware = commitmentOrConfig.fetchMiddleware; + } + + this._rpcClient = createRpcClient( + url.href, + useHttps, + httpHeaders, + fetchMiddleware, + ); this._rpcRequest = createRpcRequest(this._rpcClient); this._rpcBatchRequest = createRpcBatchRequest(this._rpcClient); - this._commitment = commitment; + this._blockhashInfo = { recentBlockhash: null, lastFetch: 0, @@ -1764,6 +1837,7 @@ export class Connection { if (url.port !== null) { url.port = String(Number(url.port) + 1); } + this._rpcWebSocket = new RpcWebSocketClient(urlFormat(url), { autoconnect: false, max_reconnects: Infinity, diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index 83cb84fc4..51cb861bf 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -88,6 +88,49 @@ describe('Connection', () => { }); } + if (mockServer) { + it('should pass HTTP headers to RPC', async () => { + const headers = { + Authorization: 'Bearer 123', + }; + + let connection = new Connection(url, { + httpHeaders: headers, + }); + + await mockRpcResponse({ + method: 'getVersion', + params: [], + value: {'solana-core': '0.20.4'}, + withHeaders: headers, + }); + + expect(await connection.getVersion()).to.be.not.null; + }); + + it('should allow middleware to augment request', async () => { + let connection = new Connection(url, { + fetchMiddleware: (url, options, fetch) => { + options.headers = Object.assign(options.headers, { + Authorization: 'Bearer 123', + }); + fetch(url, options); + }, + }); + + await mockRpcResponse({ + method: 'getVersion', + params: [], + value: {'solana-core': '0.20.4'}, + withHeaders: { + Authorization: 'Bearer 123', + }, + }); + + expect(await connection.getVersion()).to.be.not.null; + }); + } + it('get account info - not found', async () => { const account = new Account(); diff --git a/web3.js/test/mocks/rpc-http.ts b/web3.js/test/mocks/rpc-http.ts index 695fa7e7f..b8b4aef8a 100644 --- a/web3.js/test/mocks/rpc-http.ts +++ b/web3.js/test/mocks/rpc-http.ts @@ -5,7 +5,7 @@ import * as mockttp from 'mockttp'; import {mockRpcMessage} from './rpc-websockets'; import {Account, Connection, PublicKey, Transaction} from '../../src'; -import type {Commitment, RpcParams} from '../../src/connection'; +import type {Commitment, HttpHeaders, RpcParams} from '../../src/connection'; export const mockServer: mockttp.Mockttp | undefined = process.env.TEST_LIVE === undefined ? mockttp.getLocal() : undefined; @@ -64,12 +64,14 @@ export const mockRpcResponse = async ({ value, error, withContext, + withHeaders, }: { method: string; params: Array; value?: any; error?: any; withContext?: boolean; + withHeaders?: HttpHeaders; }) => { if (!mockServer) return; @@ -90,6 +92,7 @@ export const mockRpcResponse = async ({ method, params, }) + .withHeaders(withHeaders || {}) .thenReply( 200, JSON.stringify({