@percy/client 1.32.12-beta.1 → 1.32.12-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/proxy.js CHANGED
@@ -86,13 +86,17 @@ export function href(options) {
86
86
  }
87
87
  ;
88
88
 
89
- // Returns the proxy URL for a set of request options
90
- export function getProxy(options) {
91
- let proxyUrl = options.protocol === 'https:' && (process.env.https_proxy || process.env.HTTPS_PROXY) || process.env.http_proxy || process.env.HTTP_PROXY;
92
- let shouldProxy = !!proxyUrl && !hostnameMatches(stripQuotesAndSpaces(process.env.no_proxy || process.env.NO_PROXY), href(options));
93
- if (proxyUrl && typeof proxyUrl === 'string') {
94
- proxyUrl = stripQuotesAndSpaces(proxyUrl);
89
+ // Returns the proxy URL for a set of request options. An explicit `override` proxy URL is
90
+ // used as-is, without consulting the proxy env vars or NO_PROXY; the caller owns bypassing.
91
+ export function getProxy(options, override) {
92
+ let envProxyUrl = options.protocol === 'https:' && (process.env.https_proxy || process.env.HTTPS_PROXY) || process.env.http_proxy || process.env.HTTP_PROXY;
93
+ let shouldProxy = !!override || !!envProxyUrl && !hostnameMatches(stripQuotesAndSpaces(process.env.no_proxy || process.env.NO_PROXY), href(options));
94
+
95
+ // only env values may carry stray quotes/spaces; an explicit override is used verbatim
96
+ if (envProxyUrl && typeof envProxyUrl === 'string') {
97
+ envProxyUrl = stripQuotesAndSpaces(envProxyUrl);
95
98
  }
99
+ let proxyUrl = override || envProxyUrl;
96
100
  if (shouldProxy) {
97
101
  proxyUrl = new URL(proxyUrl);
98
102
  let isHttps = proxyUrl.protocol === 'https:';
@@ -120,11 +124,20 @@ export class ProxyHttpAgent extends http.Agent {
120
124
  httpsAgent = new https.Agent({
121
125
  keepAlive: true
122
126
  });
127
+
128
+ // an optional `proxy` URL takes precedence over the proxy env vars
129
+ constructor({
130
+ proxy,
131
+ ...options
132
+ } = {}) {
133
+ super(options);
134
+ this.proxy = proxy;
135
+ }
123
136
  addRequest(request, options) {
124
137
  var _request$outputData;
125
- let proxy = getProxy(options);
138
+ let proxy = getProxy(options, this.proxy);
126
139
  if (!proxy) return super.addRequest(request, options);
127
- logger('client:proxy').debug(`Proxying request: ${options.href}`);
140
+ logger('client:proxy').debug(`Proxying request: ${href(options)}`);
128
141
 
129
142
  // modify the request for proxying
130
143
  request.path = href(options);
@@ -157,15 +170,20 @@ export class ProxyHttpAgent extends http.Agent {
157
170
 
158
171
  // Proxified https agent
159
172
  export class ProxyHttpsAgent extends https.Agent {
160
- constructor(options) {
173
+ // an optional `proxy` URL takes precedence over the proxy env vars
174
+ constructor({
175
+ proxy,
176
+ ...options
177
+ } = {}) {
161
178
  // default keep-alive
162
179
  super({
163
180
  keepAlive: true,
164
181
  ...options
165
182
  });
183
+ this.proxy = proxy;
166
184
  }
167
185
  createConnection(options, callback) {
168
- let proxy = getProxy(options);
186
+ let proxy = getProxy(options, this.proxy);
169
187
  if (!proxy) return super.createConnection(options, callback);
170
188
  logger('client:proxy').debug(`Proxying request: ${href(options)}`);
171
189
 
@@ -180,8 +198,13 @@ export class ProxyHttpsAgent extends https.Agent {
180
198
 
181
199
  // start the proxy connection and setup listeners
182
200
  let socket = proxy.connect();
201
+
202
+ // destroying the socket re-emits 'error' and 'close'; only the first failure counts
203
+ let failed = false;
183
204
  let handleError = err => {
184
205
  var _err$message, _err$message2;
206
+ if (failed) return;
207
+ failed = true;
185
208
  socket.destroy(err);
186
209
  logger('client:proxy').error(`Proxying request ${href(options)} failed: ${err}`);
187
210
 
@@ -194,6 +217,12 @@ export class ProxyHttpsAgent extends https.Agent {
194
217
  callback(err);
195
218
  };
196
219
  let handleClose = () => handleError(new Error('Connection closed while sending request to upstream proxy'));
220
+
221
+ // a request's `timeout` only starts once it has a socket, which is handed over after the
222
+ // CONNECT reply; bound the handshake itself so a silent proxy cannot hang the request
223
+ let handleTimeout = () => handleError(Object.assign(new Error(`Request to ${href(options)} timed out after ${options.timeout}ms waiting for the proxy`), {
224
+ code: 'ETIMEDOUT'
225
+ }));
197
226
  let buffer = '';
198
227
  let handleData = data => {
199
228
  var _buffer$match;
@@ -205,11 +234,16 @@ export class ProxyHttpsAgent extends https.Agent {
205
234
  if (((_buffer$match = buffer.match(STATUS_REG)) === null || _buffer$match === void 0 ? void 0 : _buffer$match[1]) !== '200') {
206
235
  return handleError(new Error('Error establishing proxy connection. ' + `Response from server was: ${buffer}`));
207
236
  }
237
+
238
+ // the tunnel is established; its later close (e.g. keep-alive teardown) is not a failure
239
+ socket.off('error', handleError).off('close', handleClose).off('timeout', handleTimeout);
240
+ socket.setTimeout(0);
208
241
  options.socket = socket;
209
242
  options.servername = options.hostname;
210
243
  // callback not passed in so not to be added as a listener
211
244
  callback(null, super.createConnection(options));
212
245
  };
246
+ if (options.timeout) socket.setTimeout(options.timeout, handleTimeout);
213
247
 
214
248
  // send and handle the connect message
215
249
  socket.on('error', handleError).on('close', handleClose).on('data', handleData).write(connectMessage);
@@ -222,6 +256,7 @@ export function proxyAgentFor(url, options) {
222
256
  hostname
223
257
  } = new URL(url);
224
258
  let cachekey = `${protocol}//${hostname}`;
259
+ if (options !== null && options !== void 0 && options.proxy) cachekey += ` via ${options.proxy}`;
225
260
 
226
261
  // If we already have a cached agent, return it
227
262
  if (cache.has(cachekey)) {
@@ -231,8 +266,8 @@ export function proxyAgentFor(url, options) {
231
266
  let agent;
232
267
  const pacUrl = process.env.PERCY_PAC_FILE_URL;
233
268
 
234
- // If PAC URL is provided, use PAC proxy
235
- if (pacUrl) {
269
+ // If PAC URL is provided, use PAC proxy (an explicit proxy takes precedence)
270
+ if (pacUrl && !(options !== null && options !== void 0 && options.proxy)) {
236
271
  logger('client:proxy').info(`Using PAC file from: ${pacUrl}`);
237
272
  agent = createPacAgent(pacUrl, options);
238
273
  } else {
package/dist/utils.js CHANGED
@@ -116,7 +116,8 @@ const RETRY_ERROR_CODES = ['ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH'
116
116
  // and any received error details. Server 500 errors are retried up to 5 times at 50ms intervals by
117
117
  // default, and 404 errors may also be optionally retried. If a callback is provided, it is called
118
118
  // with the parsed response body and response details. If the callback returns a value, that value
119
- // will be returned in the final resolved promise instead of the response body.
119
+ // will be returned in the final resolved promise instead of the response body. A `proxy` URL
120
+ // routes the request through that proxy regardless of the proxy env vars.
120
121
  export async function request(url, options = {}, callback) {
121
122
  // accept `request(url, callback)`
122
123
  if (typeof options === 'function') [options, callback] = [{}, options];
@@ -129,6 +130,7 @@ export async function request(url, options = {}, callback) {
129
130
  retryNotFound,
130
131
  interval,
131
132
  noProxy,
133
+ proxy,
132
134
  buffer,
133
135
  meta = {},
134
136
  ...requestOptions
@@ -163,7 +165,9 @@ export async function request(url, options = {}, callback) {
163
165
 
164
166
  // combine request options
165
167
  Object.assign(requestOptions, {
166
- agent: requestOptions.agent || !noProxy && proxyAgentFor(url) || null,
168
+ agent: requestOptions.agent || proxy && proxyAgentFor(url, {
169
+ proxy
170
+ }) || !noProxy && proxyAgentFor(url) || null,
167
171
  path: pathname + search + hash,
168
172
  protocol,
169
173
  hostname,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/client",
3
- "version": "1.32.12-beta.1",
3
+ "version": "1.32.12-beta.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,11 +33,11 @@
33
33
  "test:coverage": "yarn test --coverage"
34
34
  },
35
35
  "dependencies": {
36
- "@percy/config": "1.32.12-beta.1",
37
- "@percy/env": "1.32.12-beta.1",
38
- "@percy/logger": "1.32.12-beta.1",
36
+ "@percy/config": "1.32.12-beta.2",
37
+ "@percy/env": "1.32.12-beta.2",
38
+ "@percy/logger": "1.32.12-beta.2",
39
39
  "pac-proxy-agent": "^7.0.2",
40
40
  "pako": "^2.1.0"
41
41
  },
42
- "gitHead": "80a7983e35606c049956af87c5f74646cb2333fa"
42
+ "gitHead": "35e95e257d7f09d763698765e429dbae0ce7d5e9"
43
43
  }