Skip to content

Commit a5c76e5

Browse files
authored
[core-rest-pipeline] Add support for custom agents. (Azure#14450)
On NodeJS it's often useful to be able to control the agent used to make a request. This change adds support for setting a custom agent on a per-request basis and also refactors ProxyPolicy to be in charge of proxy agents.
1 parent d85bff4 commit a5c76e5

File tree

5 files changed

+127
-61
lines changed

5 files changed

+127
-61
lines changed

sdk/core/core-rest-pipeline/review/core-rest-pipeline.api.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ export interface AddPipelineOptions {
1717
phase?: PipelinePhase;
1818
}
1919

20+
// @public
21+
export interface Agent {
22+
destroy(): void;
23+
maxFreeSockets: number;
24+
maxSockets: number;
25+
requests: unknown;
26+
sockets: unknown;
27+
}
28+
2029
// @public
2130
export function bearerTokenAuthenticationPolicy(options: BearerTokenAuthenticationPolicyOptions): PipelinePolicy;
2231

@@ -153,6 +162,7 @@ export interface PipelinePolicy {
153162
// @public
154163
export interface PipelineRequest {
155164
abortSignal?: AbortSignalLike;
165+
agent?: Agent;
156166
allowInsecureConnection?: boolean;
157167
body?: RequestBodyType;
158168
disableKeepAlive?: boolean;

sdk/core/core-rest-pipeline/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
export {
5+
Agent,
56
HttpClient,
67
PipelineRequest,
78
PipelineResponse,

sdk/core/core-rest-pipeline/src/interfaces.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,34 @@ export type RequestBodyType =
5656
| string
5757
| null;
5858

59+
/**
60+
* An interface compatible with NodeJS's `http.Agent`.
61+
* We want to avoid publicly re-exporting the actual interface,
62+
* since it might vary across runtime versions.
63+
*/
64+
export interface Agent {
65+
/**
66+
* Destroy any sockets that are currently in use by the agent.
67+
*/
68+
destroy(): void;
69+
/**
70+
* For agents with keepAlive enabled, this sets the maximum number of sockets that will be left open in the free state.
71+
*/
72+
maxFreeSockets: number;
73+
/**
74+
* Determines how many concurrent sockets the agent can have open per origin.
75+
*/
76+
maxSockets: number;
77+
/**
78+
* An object which contains queues of requests that have not yet been assigned to sockets.
79+
*/
80+
requests: unknown;
81+
/**
82+
* An object which contains arrays of sockets currently in use by the agent.
83+
*/
84+
sockets: unknown;
85+
}
86+
5987
/**
6088
* Metadata about a request being made by the pipeline.
6189
*/
@@ -138,6 +166,14 @@ export interface PipelineRequest {
138166

139167
/** Set to true if the request is sent over HTTP instead of HTTPS */
140168
allowInsecureConnection?: boolean;
169+
170+
/**
171+
* NODEJS ONLY
172+
*
173+
* A Node-only option to provide a custom `http.Agent`/`https.Agent`.
174+
* Does nothing when running in the browser.
175+
*/
176+
agent?: Agent;
141177
}
142178

143179
/**

sdk/core/core-rest-pipeline/src/nodeHttpClient.ts

Lines changed: 14 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@ import * as http from "http";
55
import * as https from "https";
66
import * as zlib from "zlib";
77
import { Transform } from "stream";
8-
import { HttpsProxyAgent, HttpsProxyAgentOptions } from "https-proxy-agent";
9-
import { HttpProxyAgent, HttpProxyAgentOptions } from "http-proxy-agent";
108
import { AbortController, AbortError } from "@azure/abort-controller";
119
import {
1210
HttpClient,
1311
PipelineRequest,
1412
PipelineResponse,
1513
TransferProgressEvent,
1614
HttpHeaders,
17-
RequestBodyType,
18-
ProxySettings
15+
RequestBodyType
1916
} from "./interfaces";
2017
import { createHttpHeaders } from "./httpHeaders";
2118
import { RestError } from "./restError";
@@ -67,9 +64,7 @@ class ReportTransform extends Transform {
6764
*/
6865
class NodeHttpClient implements HttpClient {
6966
private httpsKeepAliveAgent?: https.Agent;
70-
private httpsProxyAgent?: https.Agent;
7167
private httpKeepAliveAgent?: http.Agent;
72-
private httpProxyAgent?: http.Agent;
7368

7469
/**
7570
* Makes a request over an underlying transport layer and returns the response.
@@ -205,7 +200,7 @@ class NodeHttpClient implements HttpClient {
205200
throw new Error(`Cannot connect to ${request.url} while allowInsecureConnection is false.`);
206201
}
207202

208-
const agent = this.getOrCreateAgent(request, isInsecure);
203+
const agent = request.agent ?? this.getOrCreateAgent(request, isInsecure);
209204
const options: http.RequestOptions = {
210205
agent,
211206
hostname: url.hostname,
@@ -221,66 +216,25 @@ class NodeHttpClient implements HttpClient {
221216
}
222217
}
223218

224-
private getProxyAgentOptions(
225-
proxySettings: ProxySettings,
226-
requestHeaders: HttpHeaders
227-
): HttpProxyAgentOptions {
228-
let parsedProxyUrl: URL;
229-
try {
230-
parsedProxyUrl = new URL(proxySettings.host);
231-
} catch (_error) {
232-
throw new Error(
233-
`Expecting a valid host string in proxy settings, but found "${proxySettings.host}".`
234-
);
235-
}
236-
237-
const proxyAgentOptions: HttpsProxyAgentOptions = {
238-
hostname: parsedProxyUrl.hostname,
239-
port: proxySettings.port,
240-
protocol: parsedProxyUrl.protocol,
241-
headers: requestHeaders.toJSON()
242-
};
243-
if (proxySettings.username && proxySettings.password) {
244-
proxyAgentOptions.auth = `${proxySettings.username}:${proxySettings.password}`;
245-
}
246-
return proxyAgentOptions;
247-
}
248-
249219
private getOrCreateAgent(request: PipelineRequest, isInsecure: boolean): http.Agent {
250-
// At the moment, proxy settings and keepAlive are mutually
251-
// exclusive because the proxy library currently lacks the
252-
// ability to create a proxy with keepAlive turned on.
253-
const proxySettings = request.proxySettings;
254-
if (proxySettings) {
220+
if (!request.disableKeepAlive) {
255221
if (isInsecure) {
256-
if (!this.httpProxyAgent) {
257-
const proxyAgentOptions = this.getProxyAgentOptions(proxySettings, request.headers);
258-
this.httpProxyAgent = new HttpProxyAgent(proxyAgentOptions);
222+
if (!this.httpKeepAliveAgent) {
223+
this.httpKeepAliveAgent = new http.Agent({
224+
keepAlive: true
225+
});
259226
}
260-
return this.httpProxyAgent;
227+
228+
return this.httpKeepAliveAgent;
261229
} else {
262-
if (!this.httpsProxyAgent) {
263-
const proxyAgentOptions = this.getProxyAgentOptions(proxySettings, request.headers);
264-
this.httpsProxyAgent = new HttpsProxyAgent(proxyAgentOptions);
230+
if (!this.httpsKeepAliveAgent) {
231+
this.httpsKeepAliveAgent = new https.Agent({
232+
keepAlive: true
233+
});
265234
}
266-
return this.httpsProxyAgent;
267-
}
268-
} else if (!request.disableKeepAlive && !isInsecure) {
269-
if (!this.httpsKeepAliveAgent) {
270-
this.httpsKeepAliveAgent = new https.Agent({
271-
keepAlive: true
272-
});
273-
}
274235

275-
return this.httpsKeepAliveAgent;
276-
} else if (!request.disableKeepAlive && isInsecure) {
277-
if (!this.httpKeepAliveAgent) {
278-
this.httpKeepAliveAgent = new http.Agent({
279-
keepAlive: true
280-
});
236+
return this.httpsKeepAliveAgent;
281237
}
282-
283-
return this.httpKeepAliveAgent;
284238
} else if (isInsecure) {
285239
return http.globalAgent;
286240
} else {

sdk/core/core-rest-pipeline/src/policies/proxyPolicy.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { PipelineResponse, PipelineRequest, SendRequest, ProxySettings } from "../interfaces";
4+
import * as http from "http";
5+
import * as https from "https";
6+
import { HttpsProxyAgent, HttpsProxyAgentOptions } from "https-proxy-agent";
7+
import { HttpProxyAgent, HttpProxyAgentOptions } from "http-proxy-agent";
8+
import {
9+
PipelineResponse,
10+
PipelineRequest,
11+
SendRequest,
12+
ProxySettings,
13+
HttpHeaders
14+
} from "../interfaces";
515
import { PipelinePolicy } from "../pipeline";
616
import { URL } from "../util/url";
717

@@ -18,6 +28,9 @@ export const proxyPolicyName = "proxyPolicy";
1828
export const noProxyList: string[] = loadNoProxy();
1929
const byPassedList: Map<string, boolean> = new Map();
2030

31+
let httpsProxyAgent: https.Agent | undefined;
32+
let httpProxyAgent: http.Agent | undefined;
33+
2134
function getEnvironmentValue(name: string): string | undefined {
2235
if (process.env[name]) {
2336
return process.env[name];
@@ -107,6 +120,54 @@ export function getDefaultProxySettings(proxyUrl?: string): ProxySettings | unde
107120
};
108121
}
109122

123+
function getProxyAgentOptions(
124+
proxySettings: ProxySettings,
125+
requestHeaders: HttpHeaders
126+
): HttpProxyAgentOptions {
127+
let parsedProxyUrl: URL;
128+
try {
129+
parsedProxyUrl = new URL(proxySettings.host);
130+
} catch (_error) {
131+
throw new Error(
132+
`Expecting a valid host string in proxy settings, but found "${proxySettings.host}".`
133+
);
134+
}
135+
136+
const proxyAgentOptions: HttpsProxyAgentOptions = {
137+
hostname: parsedProxyUrl.hostname,
138+
port: proxySettings.port,
139+
protocol: parsedProxyUrl.protocol,
140+
headers: requestHeaders.toJSON()
141+
};
142+
if (proxySettings.username && proxySettings.password) {
143+
proxyAgentOptions.auth = `${proxySettings.username}:${proxySettings.password}`;
144+
}
145+
return proxyAgentOptions;
146+
}
147+
148+
function setProxyAgentOnRequest(request: PipelineRequest): void {
149+
const url = new URL(request.url);
150+
151+
const isInsecure = url.protocol !== "https:";
152+
153+
const proxySettings = request.proxySettings;
154+
if (proxySettings) {
155+
if (isInsecure) {
156+
if (!httpProxyAgent) {
157+
const proxyAgentOptions = getProxyAgentOptions(proxySettings, request.headers);
158+
httpProxyAgent = new HttpProxyAgent(proxyAgentOptions);
159+
}
160+
request.agent = httpProxyAgent;
161+
} else {
162+
if (!httpsProxyAgent) {
163+
const proxyAgentOptions = getProxyAgentOptions(proxySettings, request.headers);
164+
httpsProxyAgent = new HttpsProxyAgent(proxyAgentOptions);
165+
}
166+
request.agent = httpsProxyAgent;
167+
}
168+
}
169+
}
170+
110171
/**
111172
* A policy that allows one to apply proxy settings to all requests.
112173
* If not passed static settings, they will be retrieved from the HTTPS_PROXY
@@ -120,6 +181,10 @@ export function proxyPolicy(proxySettings = getDefaultProxySettings()): Pipeline
120181
if (!request.proxySettings && !isBypassed(request.url)) {
121182
request.proxySettings = proxySettings;
122183
}
184+
185+
if (request.proxySettings) {
186+
setProxyAgentOnRequest(request);
187+
}
123188
return next(request);
124189
}
125190
};

0 commit comments

Comments
 (0)