fable 3.1.71 → 3.1.72
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fable",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.72",
|
|
4
4
|
"description": "A service dependency injection, configuration and logging library.",
|
|
5
5
|
"main": "source/Fable.js",
|
|
6
6
|
"scripts": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"homepage": "https://github.com/stevenvelozo/fable",
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"pict-docuserve": "^0.1.5",
|
|
55
|
-
"quackage": "^1.
|
|
55
|
+
"quackage": "^1.2.3"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"async.eachlimit": "^0.5.2",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"fable-log": "^3.0.18",
|
|
66
66
|
"fable-serviceproviderbase": "^3.0.19",
|
|
67
67
|
"fable-settings": "^3.0.16",
|
|
68
|
-
"fable-uuid": "^3.0.
|
|
68
|
+
"fable-uuid": "^3.0.14",
|
|
69
69
|
"manyfest": "^1.0.49",
|
|
70
70
|
"simple-get": "^4.0.1"
|
|
71
71
|
}
|
|
@@ -90,13 +90,20 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
90
90
|
|
|
91
91
|
this.prepareRequestOptions = (pOptions) =>
|
|
92
92
|
{
|
|
93
|
-
|
|
93
|
+
// Mirror simple-get's protocol decision exactly: it routes through
|
|
94
|
+
// the https module if and only if the parsed URL protocol is
|
|
95
|
+
// exactly 'https:'. Everything else — http: URLs, relative URLs
|
|
96
|
+
// (which simple-get treats as no-host http requests), and option
|
|
97
|
+
// objects with no .url at all — goes through the http module.
|
|
98
|
+
// Stamping an httpsAgent on an http request makes Node throw
|
|
99
|
+
// ERR_INVALID_PROTOCOL when http.request validates agent.protocol.
|
|
100
|
+
if (typeof pOptions.url === 'string' && pOptions.url.startsWith('https:'))
|
|
94
101
|
{
|
|
95
|
-
pOptions.agent = this.
|
|
102
|
+
pOptions.agent = this.httpsAgent;
|
|
96
103
|
}
|
|
97
104
|
else
|
|
98
105
|
{
|
|
99
|
-
pOptions.agent = this.
|
|
106
|
+
pOptions.agent = this.httpAgent;
|
|
100
107
|
}
|
|
101
108
|
return tmpPreviousPrepareRequestOptions(pOptions);
|
|
102
109
|
};
|
|
@@ -148,6 +155,196 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
148
155
|
return this.prepareRequestOptions(tmpOptions);
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Extract the hostname from a URL string. Returns null for relative URLs
|
|
160
|
+
* or anything else that doesn't parse cleanly.
|
|
161
|
+
*
|
|
162
|
+
* @private
|
|
163
|
+
* @param {string} pUrl
|
|
164
|
+
* @return {string|null}
|
|
165
|
+
*/
|
|
166
|
+
_parseHostname(pUrl)
|
|
167
|
+
{
|
|
168
|
+
if (typeof pUrl !== 'string')
|
|
169
|
+
{
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try
|
|
173
|
+
{
|
|
174
|
+
return new URL(pUrl).hostname || null;
|
|
175
|
+
}
|
|
176
|
+
catch (e)
|
|
177
|
+
{
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve a Location header against the URL of the request that produced
|
|
184
|
+
* it. Handles absolute Locations (returned as-is) and RFC 7231-compliant
|
|
185
|
+
* relative Locations (resolved against the current URL).
|
|
186
|
+
*
|
|
187
|
+
* @private
|
|
188
|
+
* @param {string} pCurrentURL - The URL of the request that produced the redirect.
|
|
189
|
+
* @param {string} pLocation - The Location header value.
|
|
190
|
+
* @return {string}
|
|
191
|
+
*/
|
|
192
|
+
_resolveRedirectURL(pCurrentURL, pLocation)
|
|
193
|
+
{
|
|
194
|
+
if (typeof pLocation !== 'string' || pLocation.length === 0)
|
|
195
|
+
{
|
|
196
|
+
return pLocation;
|
|
197
|
+
}
|
|
198
|
+
try
|
|
199
|
+
{
|
|
200
|
+
return new URL(pLocation, pCurrentURL).toString();
|
|
201
|
+
}
|
|
202
|
+
catch (e)
|
|
203
|
+
{
|
|
204
|
+
// Either pCurrentURL is itself relative or pLocation is malformed.
|
|
205
|
+
// Fall back to passing it through verbatim — simple-get's parser
|
|
206
|
+
// will give the next hop a final say.
|
|
207
|
+
return pLocation;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build the options object for the next hop of a redirect chain. Applies
|
|
213
|
+
* the same hop-rewrite rules simple-get does, plus an RFC-correct relative
|
|
214
|
+
* Location resolution that simple-get itself doesn't do:
|
|
215
|
+
* - Resolve the Location against the current URL (absolute or relative).
|
|
216
|
+
* - Strip simple-get's URL-derived state (protocol/hostname/port/path/auth)
|
|
217
|
+
* so the next hop re-parses the URL cleanly.
|
|
218
|
+
* - Drop the host header (re-derived from the new URL by simple-get).
|
|
219
|
+
* - Cross-origin: drop cookie + authorization to prevent leak.
|
|
220
|
+
* - 301/302 + POST: switch to GET, drop body and content headers.
|
|
221
|
+
*
|
|
222
|
+
* @private
|
|
223
|
+
* @param {Object} pOptions - The options used for the previous hop (post-simple-get mutation).
|
|
224
|
+
* @param {import('http').IncomingMessage} pResponse - The 3xx response.
|
|
225
|
+
* @param {string|undefined} pOriginalURL - The URL of the previous hop, captured before simple-get deleted it.
|
|
226
|
+
* @param {string|null} pOriginalHost - The hostname of the previous hop, for cross-origin detection.
|
|
227
|
+
* @return {Object}
|
|
228
|
+
*/
|
|
229
|
+
_buildRedirectedOptions(pOptions, pResponse, pOriginalURL, pOriginalHost)
|
|
230
|
+
{
|
|
231
|
+
const tmpNew = Object.assign({}, pOptions);
|
|
232
|
+
tmpNew.url = this._resolveRedirectURL(pOriginalURL, pResponse.headers.location);
|
|
233
|
+
|
|
234
|
+
// Strip simple-get's own URL-derived fields so the next call re-parses cleanly.
|
|
235
|
+
delete tmpNew.protocol;
|
|
236
|
+
delete tmpNew.hostname;
|
|
237
|
+
delete tmpNew.port;
|
|
238
|
+
delete tmpNew.path;
|
|
239
|
+
delete tmpNew.auth;
|
|
240
|
+
|
|
241
|
+
// We set followRedirects=false on the previous hop to disable
|
|
242
|
+
// simple-get's auto-follow; that's our own internal flag, not caller
|
|
243
|
+
// intent. Drop it so the recursive _executeWithRedirects entry treats
|
|
244
|
+
// the next hop as another redirect-following call.
|
|
245
|
+
delete tmpNew.followRedirects;
|
|
246
|
+
|
|
247
|
+
// Headers — clone (don't mutate the caller's) and prune.
|
|
248
|
+
if (tmpNew.headers)
|
|
249
|
+
{
|
|
250
|
+
tmpNew.headers = Object.assign({}, tmpNew.headers);
|
|
251
|
+
delete tmpNew.headers.host;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Cross-origin redirect: drop cookie and authorization to prevent leak (matches simple-get #73).
|
|
255
|
+
const tmpRedirectHost = this._parseHostname(tmpNew.url);
|
|
256
|
+
if (tmpRedirectHost !== null && tmpRedirectHost !== pOriginalHost && tmpNew.headers)
|
|
257
|
+
{
|
|
258
|
+
delete tmpNew.headers.cookie;
|
|
259
|
+
delete tmpNew.headers.authorization;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 301/302 + POST → GET (matches simple-get #35 and RFC 7231 §6.4.2/6.4.3).
|
|
263
|
+
// Body and content headers come off; 307/308 preserve method/body so we leave them alone.
|
|
264
|
+
if (tmpNew.method === 'POST' && (pResponse.statusCode === 301 || pResponse.statusCode === 302))
|
|
265
|
+
{
|
|
266
|
+
tmpNew.method = 'GET';
|
|
267
|
+
if (tmpNew.headers)
|
|
268
|
+
{
|
|
269
|
+
delete tmpNew.headers['content-length'];
|
|
270
|
+
delete tmpNew.headers['content-type'];
|
|
271
|
+
}
|
|
272
|
+
delete tmpNew.body;
|
|
273
|
+
delete tmpNew.form;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return tmpNew;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Dispatch a request via simple-get, transparently following 3xx redirects
|
|
281
|
+
* until a non-redirect response or hard error.
|
|
282
|
+
*
|
|
283
|
+
* Why we drive the loop ourselves: simple-get's own redirect path
|
|
284
|
+
* (index.js lines 50-69) recurses with the original opts.agent intact, so
|
|
285
|
+
* an http→https redirect ends up calling https.request with an httpAgent
|
|
286
|
+
* (or vice versa) and Node throws ERR_INVALID_PROTOCOL synchronously.
|
|
287
|
+
* By disabling simple-get's auto-follow and running prepareRequestOptions
|
|
288
|
+
* on each hop, the agent gets re-picked to match the new URL's protocol.
|
|
289
|
+
*
|
|
290
|
+
* Caller can opt out by setting `followRedirects: false` on options
|
|
291
|
+
* (matches simple-get's contract) — in that case we hand the 3xx straight
|
|
292
|
+
* back without following.
|
|
293
|
+
*
|
|
294
|
+
* @private
|
|
295
|
+
* @param {Object} pOptions - Already passed through preRequest on the first call; on recursion, already passed through prepareRequestOptions.
|
|
296
|
+
* @param {(err?: Error, res?: import('http').IncomingMessage) => void} fCallback
|
|
297
|
+
*/
|
|
298
|
+
_executeWithRedirects(pOptions, fCallback)
|
|
299
|
+
{
|
|
300
|
+
if (pOptions.followRedirects === false)
|
|
301
|
+
{
|
|
302
|
+
return libSimpleGet(pOptions, fCallback);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Disable simple-get's own redirect loop — we own it from here.
|
|
306
|
+
pOptions.followRedirects = false;
|
|
307
|
+
const tmpOriginalURL = pOptions.url;
|
|
308
|
+
const tmpOriginalHost = this._parseHostname(tmpOriginalURL);
|
|
309
|
+
|
|
310
|
+
return libSimpleGet(pOptions, (pError, pResponse) =>
|
|
311
|
+
{
|
|
312
|
+
if (pError)
|
|
313
|
+
{
|
|
314
|
+
return fCallback(pError, pResponse);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (pResponse.statusCode < 300 || pResponse.statusCode >= 400 || !pResponse.headers.location)
|
|
318
|
+
{
|
|
319
|
+
return fCallback(null, pResponse);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 3xx with Location — drain and follow.
|
|
323
|
+
pResponse.resume();
|
|
324
|
+
|
|
325
|
+
let tmpRedirectsRemaining = (typeof pOptions.maxRedirects === 'number') ? pOptions.maxRedirects : 10;
|
|
326
|
+
if (tmpRedirectsRemaining <= 0)
|
|
327
|
+
{
|
|
328
|
+
return fCallback(new Error('too many redirects'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const tmpNextOptions = this._buildRedirectedOptions(pOptions, pResponse, tmpOriginalURL, tmpOriginalHost);
|
|
332
|
+
tmpNextOptions.maxRedirects = tmpRedirectsRemaining - 1;
|
|
333
|
+
|
|
334
|
+
// Re-run the agent picker so the next hop's agent matches the
|
|
335
|
+
// (possibly different) protocol of the redirect target. We do
|
|
336
|
+
// NOT re-run preRequest in full — RestClientURLPrefix and
|
|
337
|
+
// prepareCookies are first-call-only.
|
|
338
|
+
const tmpPreparedNext = this.prepareRequestOptions(tmpNextOptions);
|
|
339
|
+
|
|
340
|
+
if (this.TraceLog)
|
|
341
|
+
{
|
|
342
|
+
this.fable.log.debug(`--> redirect ${pResponse.statusCode} to ${tmpPreparedNext.url}`);
|
|
343
|
+
}
|
|
344
|
+
return this._executeWithRedirects(tmpPreparedNext, fCallback);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
151
348
|
executeChunkedRequest(pOptions, fCallback)
|
|
152
349
|
{
|
|
153
350
|
let tmpOptions = this.preRequest(pOptions);
|
|
@@ -159,7 +356,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
159
356
|
this.fable.log.debug(`Beginning ${tmpOptions.method} request to ${tmpOptions.url} at ${tmpOptions.RequestStartTime}`);
|
|
160
357
|
}
|
|
161
358
|
|
|
162
|
-
return
|
|
359
|
+
return this._executeWithRedirects(tmpOptions,
|
|
163
360
|
(pError, pResponse)=>
|
|
164
361
|
{
|
|
165
362
|
if (pError)
|
|
@@ -212,7 +409,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
212
409
|
tmpOptions.json = false;
|
|
213
410
|
tmpOptions.encoding = null;
|
|
214
411
|
|
|
215
|
-
return
|
|
412
|
+
return this._executeWithRedirects(tmpOptions,
|
|
216
413
|
(pError, pResponse)=>
|
|
217
414
|
{
|
|
218
415
|
if (pError)
|
|
@@ -283,7 +480,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
283
480
|
this.fable.log.debug(`Beginning ${tmpOptions.method} JSON request to ${tmpOptions.url} at ${tmpOptions.RequestStartTime}`);
|
|
284
481
|
}
|
|
285
482
|
|
|
286
|
-
return
|
|
483
|
+
return this._executeWithRedirects(tmpOptions,
|
|
287
484
|
(pError, pResponse)=>
|
|
288
485
|
{
|
|
289
486
|
if (pError)
|
|
@@ -461,7 +658,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
461
658
|
|
|
462
659
|
tmpOptions.json = false;
|
|
463
660
|
|
|
464
|
-
return
|
|
661
|
+
return this._executeWithRedirects(tmpOptions,
|
|
465
662
|
(pError, pResponse) =>
|
|
466
663
|
{
|
|
467
664
|
if (pError)
|
package/test/RestClient_test.js
CHANGED
|
@@ -278,6 +278,73 @@ suite
|
|
|
278
278
|
Expect(tmpHttp.agent).to.equal(tmpRestClient.httpAgent);
|
|
279
279
|
}
|
|
280
280
|
);
|
|
281
|
+
test
|
|
282
|
+
(
|
|
283
|
+
'Selects the http agent for relative URLs and option objects without a URL.',
|
|
284
|
+
function ()
|
|
285
|
+
{
|
|
286
|
+
// simple-get's protocol decision is `opts.protocol === 'https:'
|
|
287
|
+
// ? https : http`, so a URL with no protocol — including a
|
|
288
|
+
// relative path or a pre-parsed options object — routes through
|
|
289
|
+
// the http module. The agent we stamp on must match, otherwise
|
|
290
|
+
// Node throws ERR_INVALID_PROTOCOL on dispatch.
|
|
291
|
+
let testFable = new libFable();
|
|
292
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-AgentInject-Relative');
|
|
293
|
+
|
|
294
|
+
let tmpRelative = tmpRestClient.prepareRequestOptions({ url: '/1.0/Users/Count' });
|
|
295
|
+
let tmpNoUrl = tmpRestClient.prepareRequestOptions({ hostname: 'localhost', port: 8086, path: '/1.0/Users' });
|
|
296
|
+
|
|
297
|
+
Expect(tmpRelative.agent).to.equal(tmpRestClient.httpAgent);
|
|
298
|
+
Expect(tmpNoUrl.agent).to.equal(tmpRestClient.httpAgent);
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
test
|
|
302
|
+
(
|
|
303
|
+
'A relative URL is dispatched without throwing ERR_INVALID_PROTOCOL.',
|
|
304
|
+
function (fTestComplete)
|
|
305
|
+
{
|
|
306
|
+
// End-to-end regression: before the fix, fable stamped the
|
|
307
|
+
// httpsAgent on relative URLs while simple-get routed them
|
|
308
|
+
// through http.request, which made Node throw synchronously.
|
|
309
|
+
// We point fable at a real http server via RestClientURLPrefix
|
|
310
|
+
// so the relative URL becomes resolvable, and assert that the
|
|
311
|
+
// request completes (rather than throwing on dispatch).
|
|
312
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
313
|
+
{
|
|
314
|
+
pRes.writeHead(200, { 'Content-Type': 'application/json' });
|
|
315
|
+
pRes.end(JSON.stringify({ Path: pReq.url }));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
tmpServer.listen(0, function ()
|
|
319
|
+
{
|
|
320
|
+
var tmpPort = tmpServer.address().port;
|
|
321
|
+
var testFable = new libFable({ RestClientURLPrefix: 'http://localhost:' + tmpPort });
|
|
322
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-RelativeURL-Dispatch');
|
|
323
|
+
|
|
324
|
+
var tmpOriginalPrepare = tmpRestClient.prepareRequestOptions;
|
|
325
|
+
var tmpCapturedAgent = null;
|
|
326
|
+
tmpRestClient.prepareRequestOptions = function (pOptions)
|
|
327
|
+
{
|
|
328
|
+
let tmpResult = tmpOriginalPrepare(pOptions);
|
|
329
|
+
if (tmpCapturedAgent === null)
|
|
330
|
+
{
|
|
331
|
+
tmpCapturedAgent = tmpResult.agent;
|
|
332
|
+
}
|
|
333
|
+
return tmpResult;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
tmpRestClient.getJSON('/relative/path',
|
|
337
|
+
function (pError, pResponse, pBody)
|
|
338
|
+
{
|
|
339
|
+
Expect(pError).to.equal(null);
|
|
340
|
+
Expect(tmpCapturedAgent).to.equal(tmpRestClient.httpAgent);
|
|
341
|
+
Expect(pBody.Path).to.equal('/relative/path');
|
|
342
|
+
tmpServer.close();
|
|
343
|
+
fTestComplete();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
);
|
|
281
348
|
test
|
|
282
349
|
(
|
|
283
350
|
'Chains with previously set prepareRequestOptions.',
|
|
@@ -479,6 +546,281 @@ suite
|
|
|
479
546
|
}
|
|
480
547
|
);
|
|
481
548
|
|
|
549
|
+
suite
|
|
550
|
+
(
|
|
551
|
+
'Redirect Following',
|
|
552
|
+
function ()
|
|
553
|
+
{
|
|
554
|
+
test
|
|
555
|
+
(
|
|
556
|
+
'Follows a basic 301 redirect end-to-end.',
|
|
557
|
+
function (fTestComplete)
|
|
558
|
+
{
|
|
559
|
+
// Single server: /start returns 301 to /dest, /dest returns 200 + JSON.
|
|
560
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
561
|
+
{
|
|
562
|
+
if (pReq.url === '/start')
|
|
563
|
+
{
|
|
564
|
+
pRes.writeHead(301, { Location: '/dest' });
|
|
565
|
+
pRes.end();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
pRes.writeHead(200, { 'Content-Type': 'application/json' });
|
|
569
|
+
pRes.end(JSON.stringify({ Reached: pReq.url }));
|
|
570
|
+
});
|
|
571
|
+
tmpServer.listen(0, function ()
|
|
572
|
+
{
|
|
573
|
+
var tmpPort = tmpServer.address().port;
|
|
574
|
+
var testFable = new libFable();
|
|
575
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-Basic');
|
|
576
|
+
tmpRestClient.getJSON('http://localhost:' + tmpPort + '/start',
|
|
577
|
+
function (pError, pResponse, pBody)
|
|
578
|
+
{
|
|
579
|
+
Expect(pError).to.equal(null);
|
|
580
|
+
Expect(pBody.Reached).to.equal('/dest');
|
|
581
|
+
tmpServer.close();
|
|
582
|
+
fTestComplete();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
test
|
|
588
|
+
(
|
|
589
|
+
'Re-picks the agent on a protocol-changing redirect.',
|
|
590
|
+
function (fTestComplete)
|
|
591
|
+
{
|
|
592
|
+
// http server redirects to an https URL. We don't need the https
|
|
593
|
+
// destination to be reachable — we just need to verify that
|
|
594
|
+
// prepareRequestOptions runs again on the redirect target and
|
|
595
|
+
// stamps the httpsAgent on the second hop.
|
|
596
|
+
var tmpAssignedAgents = [];
|
|
597
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
598
|
+
{
|
|
599
|
+
pRes.writeHead(302, { Location: 'https://nowhere.invalid/x' });
|
|
600
|
+
pRes.end();
|
|
601
|
+
});
|
|
602
|
+
tmpServer.listen(0, function ()
|
|
603
|
+
{
|
|
604
|
+
var tmpPort = tmpServer.address().port;
|
|
605
|
+
var testFable = new libFable();
|
|
606
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-ProtoSwitch');
|
|
607
|
+
|
|
608
|
+
var tmpOriginalPrepare = tmpRestClient.prepareRequestOptions;
|
|
609
|
+
tmpRestClient.prepareRequestOptions = function (pOptions)
|
|
610
|
+
{
|
|
611
|
+
var tmpResult = tmpOriginalPrepare(pOptions);
|
|
612
|
+
tmpAssignedAgents.push({ url: tmpResult.url, agent: tmpResult.agent });
|
|
613
|
+
return tmpResult;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
tmpRestClient.getJSON('http://localhost:' + tmpPort + '/start',
|
|
617
|
+
function (pError)
|
|
618
|
+
{
|
|
619
|
+
// The second hop will fail (DNS lookup of nowhere.invalid)
|
|
620
|
+
// but the agent re-pick must have already happened
|
|
621
|
+
// before that — and crucially, must NOT have thrown
|
|
622
|
+
// ERR_INVALID_PROTOCOL synchronously like the old code.
|
|
623
|
+
Expect(tmpAssignedAgents.length).to.be.at.least(2);
|
|
624
|
+
Expect(tmpAssignedAgents[0].agent).to.equal(tmpRestClient.httpAgent);
|
|
625
|
+
Expect(tmpAssignedAgents[1].agent).to.equal(tmpRestClient.httpsAgent);
|
|
626
|
+
Expect(tmpAssignedAgents[1].url).to.equal('https://nowhere.invalid/x');
|
|
627
|
+
tmpServer.close();
|
|
628
|
+
fTestComplete();
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
test
|
|
634
|
+
(
|
|
635
|
+
'Bails with "too many redirects" when the budget is exceeded.',
|
|
636
|
+
function (fTestComplete)
|
|
637
|
+
{
|
|
638
|
+
// Self-redirecting server. Cap at 3 hops so the test stays quick.
|
|
639
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
640
|
+
{
|
|
641
|
+
pRes.writeHead(302, { Location: pReq.url });
|
|
642
|
+
pRes.end();
|
|
643
|
+
});
|
|
644
|
+
tmpServer.listen(0, function ()
|
|
645
|
+
{
|
|
646
|
+
var tmpPort = tmpServer.address().port;
|
|
647
|
+
var testFable = new libFable();
|
|
648
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-Loop');
|
|
649
|
+
tmpRestClient.getJSON({ url: 'http://localhost:' + tmpPort + '/loop', maxRedirects: 3 },
|
|
650
|
+
function (pError)
|
|
651
|
+
{
|
|
652
|
+
Expect(pError).to.be.an.instanceof(Error);
|
|
653
|
+
Expect(pError.message).to.equal('too many redirects');
|
|
654
|
+
tmpServer.close();
|
|
655
|
+
fTestComplete();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
);
|
|
660
|
+
test
|
|
661
|
+
(
|
|
662
|
+
'Drops cookie and authorization on cross-origin redirect.',
|
|
663
|
+
function (fTestComplete)
|
|
664
|
+
{
|
|
665
|
+
// Two servers on different ports. We use 127.0.0.1 vs localhost
|
|
666
|
+
// to make them count as different hostnames per
|
|
667
|
+
// _parseHostname's URL-string check.
|
|
668
|
+
var tmpReceivedAtA = null;
|
|
669
|
+
var tmpReceivedAtB = null;
|
|
670
|
+
var tmpServerB = libHTTP.createServer(function (pReq, pRes)
|
|
671
|
+
{
|
|
672
|
+
tmpReceivedAtB = pReq.headers;
|
|
673
|
+
pRes.writeHead(200, { 'Content-Type': 'application/json' });
|
|
674
|
+
pRes.end(JSON.stringify({ ok: true }));
|
|
675
|
+
});
|
|
676
|
+
tmpServerB.listen(0, function ()
|
|
677
|
+
{
|
|
678
|
+
var tmpPortB = tmpServerB.address().port;
|
|
679
|
+
var tmpServerA = libHTTP.createServer(function (pReq, pRes)
|
|
680
|
+
{
|
|
681
|
+
tmpReceivedAtA = pReq.headers;
|
|
682
|
+
pRes.writeHead(302, { Location: 'http://127.0.0.1:' + tmpPortB + '/dest' });
|
|
683
|
+
pRes.end();
|
|
684
|
+
});
|
|
685
|
+
tmpServerA.listen(0, function ()
|
|
686
|
+
{
|
|
687
|
+
var tmpPortA = tmpServerA.address().port;
|
|
688
|
+
var testFable = new libFable();
|
|
689
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-CrossOrigin');
|
|
690
|
+
tmpRestClient.getJSON({
|
|
691
|
+
url: 'http://localhost:' + tmpPortA + '/start',
|
|
692
|
+
headers: { cookie: 'session=abc', authorization: 'Bearer xyz' }
|
|
693
|
+
},
|
|
694
|
+
function (pError, pResponse, pBody)
|
|
695
|
+
{
|
|
696
|
+
Expect(pError).to.equal(null);
|
|
697
|
+
Expect(pBody.ok).to.equal(true);
|
|
698
|
+
Expect(tmpReceivedAtA.cookie).to.equal('session=abc');
|
|
699
|
+
Expect(tmpReceivedAtA.authorization).to.equal('Bearer xyz');
|
|
700
|
+
Expect(tmpReceivedAtB.cookie).to.equal(undefined);
|
|
701
|
+
Expect(tmpReceivedAtB.authorization).to.equal(undefined);
|
|
702
|
+
tmpServerA.close();
|
|
703
|
+
tmpServerB.close();
|
|
704
|
+
fTestComplete();
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
);
|
|
710
|
+
test
|
|
711
|
+
(
|
|
712
|
+
'Converts POST to GET on a 301/302 redirect and drops the body.',
|
|
713
|
+
function (fTestComplete)
|
|
714
|
+
{
|
|
715
|
+
var tmpDestRequest = null;
|
|
716
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
717
|
+
{
|
|
718
|
+
if (pReq.url === '/start')
|
|
719
|
+
{
|
|
720
|
+
pRes.writeHead(301, { Location: '/dest' });
|
|
721
|
+
pRes.end();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
tmpDestRequest = { method: pReq.method, headers: pReq.headers };
|
|
725
|
+
var tmpBody = '';
|
|
726
|
+
pReq.on('data', function (pChunk) { tmpBody += pChunk; });
|
|
727
|
+
pReq.on('end', function ()
|
|
728
|
+
{
|
|
729
|
+
tmpDestRequest.body = tmpBody;
|
|
730
|
+
pRes.writeHead(200, { 'Content-Type': 'application/json' });
|
|
731
|
+
pRes.end(JSON.stringify({ ok: true }));
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
tmpServer.listen(0, function ()
|
|
735
|
+
{
|
|
736
|
+
var tmpPort = tmpServer.address().port;
|
|
737
|
+
var testFable = new libFable();
|
|
738
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-PostToGet');
|
|
739
|
+
tmpRestClient.postJSON({ url: 'http://localhost:' + tmpPort + '/start', body: { foo: 'bar' } },
|
|
740
|
+
function (pError, pResponse, pBody)
|
|
741
|
+
{
|
|
742
|
+
Expect(pError).to.equal(null);
|
|
743
|
+
Expect(pBody.ok).to.equal(true);
|
|
744
|
+
Expect(tmpDestRequest.method).to.equal('GET');
|
|
745
|
+
Expect(tmpDestRequest.body).to.equal('');
|
|
746
|
+
Expect(tmpDestRequest.headers['content-type']).to.equal(undefined);
|
|
747
|
+
Expect(tmpDestRequest.headers['content-length']).to.equal(undefined);
|
|
748
|
+
tmpServer.close();
|
|
749
|
+
fTestComplete();
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
);
|
|
754
|
+
test
|
|
755
|
+
(
|
|
756
|
+
'Resolves a relative Location header against the current URL.',
|
|
757
|
+
function (fTestComplete)
|
|
758
|
+
{
|
|
759
|
+
// Server returns a relative Location ("dest" instead of "/dest").
|
|
760
|
+
// RFC 7231 allows this; the next hop's URL must be resolved
|
|
761
|
+
// against the URL that produced the redirect.
|
|
762
|
+
var tmpDestRequestURL = null;
|
|
763
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
764
|
+
{
|
|
765
|
+
if (pReq.url === '/section/start')
|
|
766
|
+
{
|
|
767
|
+
pRes.writeHead(302, { Location: 'dest' });
|
|
768
|
+
pRes.end();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
tmpDestRequestURL = pReq.url;
|
|
772
|
+
pRes.writeHead(200, { 'Content-Type': 'application/json' });
|
|
773
|
+
pRes.end(JSON.stringify({ ok: true }));
|
|
774
|
+
});
|
|
775
|
+
tmpServer.listen(0, function ()
|
|
776
|
+
{
|
|
777
|
+
var tmpPort = tmpServer.address().port;
|
|
778
|
+
var testFable = new libFable();
|
|
779
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-RelativeLocation');
|
|
780
|
+
tmpRestClient.getJSON('http://localhost:' + tmpPort + '/section/start',
|
|
781
|
+
function (pError, pResponse, pBody)
|
|
782
|
+
{
|
|
783
|
+
Expect(pError).to.equal(null);
|
|
784
|
+
Expect(pBody.ok).to.equal(true);
|
|
785
|
+
Expect(tmpDestRequestURL).to.equal('/section/dest');
|
|
786
|
+
tmpServer.close();
|
|
787
|
+
fTestComplete();
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
);
|
|
792
|
+
test
|
|
793
|
+
(
|
|
794
|
+
'Honors followRedirects: false by handing the 3xx straight back.',
|
|
795
|
+
function (fTestComplete)
|
|
796
|
+
{
|
|
797
|
+
// When the caller opts out, we shouldn't follow the redirect —
|
|
798
|
+
// the JSON parser will fail because the 3xx body is empty,
|
|
799
|
+
// which is the expected pre-fix behavior.
|
|
800
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
801
|
+
{
|
|
802
|
+
pRes.writeHead(302, { Location: '/dest' });
|
|
803
|
+
pRes.end();
|
|
804
|
+
});
|
|
805
|
+
tmpServer.listen(0, function ()
|
|
806
|
+
{
|
|
807
|
+
var tmpPort = tmpServer.address().port;
|
|
808
|
+
var testFable = new libFable();
|
|
809
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-Redirect-OptOut');
|
|
810
|
+
tmpRestClient.getJSON({ url: 'http://localhost:' + tmpPort + '/start', followRedirects: false },
|
|
811
|
+
function (pError, pResponse)
|
|
812
|
+
{
|
|
813
|
+
Expect(pResponse.statusCode).to.equal(302);
|
|
814
|
+
Expect(pResponse.headers.location).to.equal('/dest');
|
|
815
|
+
tmpServer.close();
|
|
816
|
+
fTestComplete();
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
|
|
482
824
|
suite
|
|
483
825
|
(
|
|
484
826
|
'Error Handling',
|