fable 3.1.70 → 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 +3 -3
- package/source/services/Fable-Service-RestClient.js +255 -16
- package/test/RestClient_test.js +552 -28
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
|
}
|
|
@@ -27,42 +27,83 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
27
27
|
// of the request options before they are passed to the request library.
|
|
28
28
|
this.prepareRequestOptions = (pOptions) => { return pOptions; };
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
30
|
+
// Default per-request timeout (ms). Applied in preRequest when a caller
|
|
31
|
+
// does not supply their own. Node 20+ installs a ~5s socket timeout on
|
|
32
|
+
// http.globalAgent that aborts legitimately long-running requests; any
|
|
33
|
+
// explicit `timeout` on the request options takes that default out of
|
|
34
|
+
// play. See the "Request Timeout" test suite for the behaviors covered.
|
|
35
|
+
if (typeof this.options.RequestTimeout === 'number')
|
|
36
|
+
{
|
|
37
|
+
this.defaultRequestTimeout = this.options.RequestTimeout;
|
|
38
|
+
}
|
|
39
|
+
else if (typeof this.fable.settings.RestClientRequestTimeout === 'number')
|
|
40
|
+
{
|
|
41
|
+
this.defaultRequestTimeout = this.fable.settings.RestClientRequestTimeout;
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
{
|
|
45
|
+
this.defaultRequestTimeout = 60000;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Always install our own http/https agents so every request bypasses
|
|
49
|
+
// http.globalAgent (and its Node 20+ mystery socket timeout). The
|
|
50
|
+
// KeepAlive flag only controls whether keepAlive is enabled on our own
|
|
51
|
+
// agents, not whether we have agents at all. Additional tuning
|
|
52
|
+
// (maxSockets, agent timeout, etc.) flows through KeepAliveAgentOptions.
|
|
53
|
+
let tmpKeepAlive = Boolean(this.options.KeepAlive || this.fable.settings.RestClientKeepAlive);
|
|
54
|
+
let tmpAgentOptions = Object.assign({}, this.options.KeepAliveAgentOptions);
|
|
34
55
|
if (tmpKeepAlive)
|
|
35
56
|
{
|
|
36
|
-
|
|
57
|
+
tmpAgentOptions.keepAlive = true;
|
|
37
58
|
}
|
|
59
|
+
this._installHttpAgents(tmpAgentOptions);
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
/**
|
|
41
63
|
* Initialize HTTP keep-alive agents and wire them into prepareRequestOptions.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
64
|
+
* Back-compat entry point: always forces keepAlive on. Prefer configuring
|
|
65
|
+
* the RestClient via the KeepAlive / KeepAliveAgentOptions constructor
|
|
66
|
+
* options instead of calling this method directly.
|
|
44
67
|
*
|
|
45
68
|
* @param {Object} [pAgentOptions] - Additional options passed to the Http/Https Agent constructors (e.g. timeout).
|
|
46
69
|
*/
|
|
47
70
|
initializeKeepAliveAgent(pAgentOptions)
|
|
48
71
|
{
|
|
49
72
|
let tmpAgentOptions = Object.assign({ keepAlive: true }, pAgentOptions);
|
|
73
|
+
this._installHttpAgents(tmpAgentOptions);
|
|
74
|
+
}
|
|
50
75
|
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Construct http/https Agents from the given options and wire them into
|
|
78
|
+
* prepareRequestOptions so every request carries an explicit agent.
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} pAgentOptions - Options passed directly to the Http/Https Agent constructors.
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
_installHttpAgents(pAgentOptions)
|
|
84
|
+
{
|
|
85
|
+
this.httpAgent = new libHttp.Agent(pAgentOptions);
|
|
86
|
+
this.httpsAgent = new libHttps.Agent(pAgentOptions);
|
|
53
87
|
|
|
54
88
|
// Capture any previously set prepareRequestOptions so we can chain
|
|
55
89
|
let tmpPreviousPrepareRequestOptions = this.prepareRequestOptions;
|
|
56
90
|
|
|
57
91
|
this.prepareRequestOptions = (pOptions) =>
|
|
58
92
|
{
|
|
59
|
-
|
|
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:'))
|
|
60
101
|
{
|
|
61
|
-
pOptions.agent = this.
|
|
102
|
+
pOptions.agent = this.httpsAgent;
|
|
62
103
|
}
|
|
63
104
|
else
|
|
64
105
|
{
|
|
65
|
-
pOptions.agent = this.
|
|
106
|
+
pOptions.agent = this.httpAgent;
|
|
66
107
|
}
|
|
67
108
|
return tmpPreviousPrepareRequestOptions(pOptions);
|
|
68
109
|
};
|
|
@@ -103,9 +144,207 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
103
144
|
tmpOptions.url = this.fable.settings.RestClientURLPrefix + tmpOptions.url;
|
|
104
145
|
}
|
|
105
146
|
|
|
147
|
+
// Apply the default request timeout when the caller hasn't supplied
|
|
148
|
+
// one. Setting any numeric value (including 0) suppresses the Node 20+
|
|
149
|
+
// http.globalAgent ~5s socket timeout.
|
|
150
|
+
if (typeof tmpOptions.timeout !== 'number')
|
|
151
|
+
{
|
|
152
|
+
tmpOptions.timeout = this.defaultRequestTimeout;
|
|
153
|
+
}
|
|
154
|
+
|
|
106
155
|
return this.prepareRequestOptions(tmpOptions);
|
|
107
156
|
}
|
|
108
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
|
+
|
|
109
348
|
executeChunkedRequest(pOptions, fCallback)
|
|
110
349
|
{
|
|
111
350
|
let tmpOptions = this.preRequest(pOptions);
|
|
@@ -117,7 +356,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
117
356
|
this.fable.log.debug(`Beginning ${tmpOptions.method} request to ${tmpOptions.url} at ${tmpOptions.RequestStartTime}`);
|
|
118
357
|
}
|
|
119
358
|
|
|
120
|
-
return
|
|
359
|
+
return this._executeWithRedirects(tmpOptions,
|
|
121
360
|
(pError, pResponse)=>
|
|
122
361
|
{
|
|
123
362
|
if (pError)
|
|
@@ -170,7 +409,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
170
409
|
tmpOptions.json = false;
|
|
171
410
|
tmpOptions.encoding = null;
|
|
172
411
|
|
|
173
|
-
return
|
|
412
|
+
return this._executeWithRedirects(tmpOptions,
|
|
174
413
|
(pError, pResponse)=>
|
|
175
414
|
{
|
|
176
415
|
if (pError)
|
|
@@ -241,7 +480,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
241
480
|
this.fable.log.debug(`Beginning ${tmpOptions.method} JSON request to ${tmpOptions.url} at ${tmpOptions.RequestStartTime}`);
|
|
242
481
|
}
|
|
243
482
|
|
|
244
|
-
return
|
|
483
|
+
return this._executeWithRedirects(tmpOptions,
|
|
245
484
|
(pError, pResponse)=>
|
|
246
485
|
{
|
|
247
486
|
if (pError)
|
|
@@ -419,7 +658,7 @@ class FableServiceRestClient extends libFableServiceBase
|
|
|
419
658
|
|
|
420
659
|
tmpOptions.json = false;
|
|
421
660
|
|
|
422
|
-
return
|
|
661
|
+
return this._executeWithRedirects(tmpOptions,
|
|
423
662
|
(pError, pResponse) =>
|
|
424
663
|
{
|
|
425
664
|
if (pError)
|
package/test/RestClient_test.js
CHANGED
|
@@ -148,73 +148,84 @@ suite
|
|
|
148
148
|
|
|
149
149
|
suite
|
|
150
150
|
(
|
|
151
|
-
'
|
|
151
|
+
'HTTP Agent',
|
|
152
152
|
function ()
|
|
153
153
|
{
|
|
154
154
|
test
|
|
155
155
|
(
|
|
156
|
-
'
|
|
156
|
+
'Always installs http/https agents, even when KeepAlive is not set.',
|
|
157
157
|
function ()
|
|
158
158
|
{
|
|
159
|
+
// Without an explicit agent, requests would fall through to
|
|
160
|
+
// http.globalAgent and hit the Node 20+ ~5s socket timeout.
|
|
159
161
|
let testFable = new libFable();
|
|
160
|
-
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {
|
|
162
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-DefaultAgent');
|
|
161
163
|
|
|
162
164
|
Expect(tmpRestClient.httpAgent).to.be.an('object');
|
|
163
165
|
Expect(tmpRestClient.httpsAgent).to.be.an('object');
|
|
166
|
+
Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
|
|
167
|
+
Expect(Boolean(tmpRestClient.httpsAgent.keepAlive)).to.equal(false);
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
test
|
|
171
|
+
(
|
|
172
|
+
'Enables keepAlive on agents when KeepAlive option is set.',
|
|
173
|
+
function ()
|
|
174
|
+
{
|
|
175
|
+
let testFable = new libFable();
|
|
176
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-Options');
|
|
177
|
+
|
|
164
178
|
Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
|
|
165
179
|
Expect(tmpRestClient.httpsAgent.keepAlive).to.equal(true);
|
|
166
180
|
}
|
|
167
181
|
);
|
|
168
182
|
test
|
|
169
183
|
(
|
|
170
|
-
'
|
|
184
|
+
'Enables keepAlive on agents via fable settings.',
|
|
171
185
|
function ()
|
|
172
186
|
{
|
|
173
187
|
let testFable = new libFable({ RestClientKeepAlive: true });
|
|
174
188
|
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-KeepAlive-Settings');
|
|
175
189
|
|
|
176
|
-
Expect(tmpRestClient.httpAgent).to.be.an('object');
|
|
177
|
-
Expect(tmpRestClient.httpsAgent).to.be.an('object');
|
|
178
190
|
Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
|
|
179
191
|
Expect(tmpRestClient.httpsAgent.keepAlive).to.equal(true);
|
|
180
192
|
}
|
|
181
193
|
);
|
|
182
194
|
test
|
|
183
195
|
(
|
|
184
|
-
'
|
|
196
|
+
'Passes additional agent options through KeepAliveAgentOptions.',
|
|
185
197
|
function ()
|
|
186
198
|
{
|
|
187
199
|
let testFable = new libFable();
|
|
188
200
|
let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
|
|
189
201
|
{
|
|
190
202
|
KeepAlive: true,
|
|
191
|
-
KeepAliveAgentOptions: { timeout: 300000 }
|
|
203
|
+
KeepAliveAgentOptions: { timeout: 300000, maxSockets: 32 }
|
|
192
204
|
}, 'RestClient-KeepAlive-AgentOpts');
|
|
193
205
|
|
|
194
|
-
Expect(tmpRestClient.httpAgent).to.be.an('object');
|
|
195
|
-
Expect(tmpRestClient.httpsAgent).to.be.an('object');
|
|
196
206
|
Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
|
|
197
|
-
Expect(tmpRestClient.httpsAgent.keepAlive).to.equal(true);
|
|
198
|
-
// Verify the custom timeout was passed through
|
|
199
207
|
Expect(tmpRestClient.httpAgent.options.timeout).to.equal(300000);
|
|
208
|
+
Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(32);
|
|
200
209
|
Expect(tmpRestClient.httpsAgent.options.timeout).to.equal(300000);
|
|
201
210
|
}
|
|
202
211
|
);
|
|
203
|
-
|
|
212
|
+
test
|
|
204
213
|
(
|
|
205
|
-
'
|
|
214
|
+
'KeepAliveAgentOptions apply even when KeepAlive is not enabled.',
|
|
206
215
|
function ()
|
|
207
216
|
{
|
|
217
|
+
// Tuning options still flow through when keepAlive is off.
|
|
208
218
|
let testFable = new libFable();
|
|
209
|
-
let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
|
|
219
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient',
|
|
220
|
+
{ KeepAliveAgentOptions: { maxSockets: 8 } }, 'RestClient-AgentOpts-NoKeepAlive');
|
|
210
221
|
|
|
211
|
-
Expect(tmpRestClient.httpAgent).to.equal(
|
|
212
|
-
Expect(tmpRestClient.
|
|
222
|
+
Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
|
|
223
|
+
Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(8);
|
|
213
224
|
}
|
|
214
225
|
);
|
|
215
226
|
test
|
|
216
227
|
(
|
|
217
|
-
'
|
|
228
|
+
'Injects the http agent on http:// URLs.',
|
|
218
229
|
function (fTestComplete)
|
|
219
230
|
{
|
|
220
231
|
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
@@ -227,9 +238,8 @@ suite
|
|
|
227
238
|
{
|
|
228
239
|
var tmpPort = tmpServer.address().port;
|
|
229
240
|
var testFable = new libFable();
|
|
230
|
-
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {
|
|
241
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-AgentInject-HTTP');
|
|
231
242
|
|
|
232
|
-
// Wrap prepareRequestOptions to capture the final options
|
|
233
243
|
var tmpOriginalPrepare = tmpRestClient.prepareRequestOptions;
|
|
234
244
|
var tmpCapturedAgent = null;
|
|
235
245
|
tmpRestClient.prepareRequestOptions = function (pOptions)
|
|
@@ -243,7 +253,6 @@ suite
|
|
|
243
253
|
function (pError, pResponse, pBody)
|
|
244
254
|
{
|
|
245
255
|
Expect(tmpCapturedAgent).to.equal(tmpRestClient.httpAgent);
|
|
246
|
-
Expect(pBody).to.be.an('object');
|
|
247
256
|
Expect(pBody.OK).to.equal(true);
|
|
248
257
|
tmpServer.close();
|
|
249
258
|
fTestComplete();
|
|
@@ -253,7 +262,92 @@ suite
|
|
|
253
262
|
);
|
|
254
263
|
test
|
|
255
264
|
(
|
|
256
|
-
'
|
|
265
|
+
'Selects the https agent for https:// URLs.',
|
|
266
|
+
function ()
|
|
267
|
+
{
|
|
268
|
+
// We don't need a live HTTPS server to verify the selection
|
|
269
|
+
// logic — invoke prepareRequestOptions directly and inspect
|
|
270
|
+
// which agent was chosen.
|
|
271
|
+
let testFable = new libFable();
|
|
272
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-AgentInject-HTTPS');
|
|
273
|
+
|
|
274
|
+
let tmpHttps = tmpRestClient.prepareRequestOptions({ url: 'https://example.com/x' });
|
|
275
|
+
let tmpHttp = tmpRestClient.prepareRequestOptions({ url: 'http://example.com/x' });
|
|
276
|
+
|
|
277
|
+
Expect(tmpHttps.agent).to.equal(tmpRestClient.httpsAgent);
|
|
278
|
+
Expect(tmpHttp.agent).to.equal(tmpRestClient.httpAgent);
|
|
279
|
+
}
|
|
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
|
+
);
|
|
348
|
+
test
|
|
349
|
+
(
|
|
350
|
+
'Chains with previously set prepareRequestOptions.',
|
|
257
351
|
function (fTestComplete)
|
|
258
352
|
{
|
|
259
353
|
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
@@ -268,13 +362,10 @@ suite
|
|
|
268
362
|
var testFable = new libFable();
|
|
269
363
|
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { KeepAlive: true }, 'RestClient-KeepAlive-Chain');
|
|
270
364
|
|
|
271
|
-
|
|
272
|
-
var tmpKeepAlivePrepare = tmpRestClient.prepareRequestOptions;
|
|
365
|
+
var tmpInstalledPrepare = tmpRestClient.prepareRequestOptions;
|
|
273
366
|
tmpRestClient.prepareRequestOptions = function (pOptions)
|
|
274
367
|
{
|
|
275
|
-
|
|
276
|
-
let tmpResult = tmpKeepAlivePrepare(pOptions);
|
|
277
|
-
// Then add custom header
|
|
368
|
+
let tmpResult = tmpInstalledPrepare(pOptions);
|
|
278
369
|
if (!tmpResult.headers)
|
|
279
370
|
{
|
|
280
371
|
tmpResult.headers = {};
|
|
@@ -286,7 +377,6 @@ suite
|
|
|
286
377
|
tmpRestClient.getJSON('http://localhost:' + tmpPort + '/test',
|
|
287
378
|
function (pError, pResponse, pBody)
|
|
288
379
|
{
|
|
289
|
-
Expect(pBody).to.be.an('object');
|
|
290
380
|
Expect(pBody.CustomHeader).to.equal('test-value');
|
|
291
381
|
tmpServer.close();
|
|
292
382
|
fTestComplete();
|
|
@@ -294,6 +384,440 @@ suite
|
|
|
294
384
|
});
|
|
295
385
|
}
|
|
296
386
|
);
|
|
387
|
+
test
|
|
388
|
+
(
|
|
389
|
+
'initializeKeepAliveAgent remains a back-compat entry point.',
|
|
390
|
+
function ()
|
|
391
|
+
{
|
|
392
|
+
let testFable = new libFable();
|
|
393
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-BackCompat');
|
|
394
|
+
|
|
395
|
+
// Default constructor: no keepAlive
|
|
396
|
+
Expect(Boolean(tmpRestClient.httpAgent.keepAlive)).to.equal(false);
|
|
397
|
+
|
|
398
|
+
// Calling the legacy method flips keepAlive on
|
|
399
|
+
tmpRestClient.initializeKeepAliveAgent({ maxSockets: 4 });
|
|
400
|
+
Expect(tmpRestClient.httpAgent.keepAlive).to.equal(true);
|
|
401
|
+
Expect(tmpRestClient.httpAgent.options.maxSockets).to.equal(4);
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
suite
|
|
408
|
+
(
|
|
409
|
+
'Request Timeout',
|
|
410
|
+
function ()
|
|
411
|
+
{
|
|
412
|
+
// Helper: spin up a local HTTP server that never responds so we can
|
|
413
|
+
// verify that the simple-get 'Request timed out' path fires on our
|
|
414
|
+
// configured timeout rather than any ambient http.globalAgent default.
|
|
415
|
+
var createHangingServer = function (fOnListen)
|
|
416
|
+
{
|
|
417
|
+
var tmpServer = libHTTP.createServer(function (pReq, pRes)
|
|
418
|
+
{
|
|
419
|
+
// Intentionally never write or end the response
|
|
420
|
+
});
|
|
421
|
+
tmpServer.listen(0, function ()
|
|
422
|
+
{
|
|
423
|
+
fOnListen(tmpServer, tmpServer.address().port);
|
|
424
|
+
});
|
|
425
|
+
return tmpServer;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
test
|
|
429
|
+
(
|
|
430
|
+
'Applies the library default timeout (60000ms) when none is supplied.',
|
|
431
|
+
function ()
|
|
432
|
+
{
|
|
433
|
+
let testFable = new libFable();
|
|
434
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-DefaultTimeout');
|
|
435
|
+
|
|
436
|
+
Expect(tmpRestClient.defaultRequestTimeout).to.equal(60000);
|
|
437
|
+
|
|
438
|
+
let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET' });
|
|
439
|
+
Expect(tmpPrepared.timeout).to.equal(60000);
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
test
|
|
443
|
+
(
|
|
444
|
+
'RequestTimeout constructor option overrides the library default.',
|
|
445
|
+
function ()
|
|
446
|
+
{
|
|
447
|
+
let testFable = new libFable();
|
|
448
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 60000 }, 'RestClient-OptTimeout');
|
|
449
|
+
|
|
450
|
+
Expect(tmpRestClient.defaultRequestTimeout).to.equal(60000);
|
|
451
|
+
|
|
452
|
+
let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET' });
|
|
453
|
+
Expect(tmpPrepared.timeout).to.equal(60000);
|
|
454
|
+
}
|
|
455
|
+
);
|
|
456
|
+
test
|
|
457
|
+
(
|
|
458
|
+
'RestClientRequestTimeout fable setting overrides the library default.',
|
|
459
|
+
function ()
|
|
460
|
+
{
|
|
461
|
+
let testFable = new libFable({ RestClientRequestTimeout: 45000 });
|
|
462
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-SettingTimeout');
|
|
463
|
+
|
|
464
|
+
Expect(tmpRestClient.defaultRequestTimeout).to.equal(45000);
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
test
|
|
468
|
+
(
|
|
469
|
+
'Constructor option takes precedence over fable setting.',
|
|
470
|
+
function ()
|
|
471
|
+
{
|
|
472
|
+
let testFable = new libFable({ RestClientRequestTimeout: 45000 });
|
|
473
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 15000 }, 'RestClient-TimeoutPrecedence');
|
|
474
|
+
|
|
475
|
+
Expect(tmpRestClient.defaultRequestTimeout).to.equal(15000);
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
test
|
|
479
|
+
(
|
|
480
|
+
'Caller-supplied timeout on the request options is preserved.',
|
|
481
|
+
function ()
|
|
482
|
+
{
|
|
483
|
+
let testFable = new libFable();
|
|
484
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-CallerTimeout');
|
|
485
|
+
|
|
486
|
+
let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET', timeout: 1234 });
|
|
487
|
+
Expect(tmpPrepared.timeout).to.equal(1234);
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
test
|
|
491
|
+
(
|
|
492
|
+
'Explicit timeout of 0 on the request options is preserved.',
|
|
493
|
+
function ()
|
|
494
|
+
{
|
|
495
|
+
// 0 is a valid numeric value — it signals "override the mystery
|
|
496
|
+
// default with no timeout" and must not be replaced.
|
|
497
|
+
let testFable = new libFable();
|
|
498
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', {}, 'RestClient-ZeroTimeout');
|
|
499
|
+
|
|
500
|
+
let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET', timeout: 0 });
|
|
501
|
+
Expect(tmpPrepared.timeout).to.equal(0);
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
test
|
|
505
|
+
(
|
|
506
|
+
'RequestTimeout value of 0 is honored.',
|
|
507
|
+
function ()
|
|
508
|
+
{
|
|
509
|
+
let testFable = new libFable();
|
|
510
|
+
let tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 0 }, 'RestClient-ZeroOptTimeout');
|
|
511
|
+
|
|
512
|
+
Expect(tmpRestClient.defaultRequestTimeout).to.equal(0);
|
|
513
|
+
|
|
514
|
+
let tmpPrepared = tmpRestClient.preRequest({ url: 'http://example.com/x', method: 'GET' });
|
|
515
|
+
Expect(tmpPrepared.timeout).to.equal(0);
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
test
|
|
519
|
+
(
|
|
520
|
+
'A short RequestTimeout actually aborts a hanging request.',
|
|
521
|
+
function (fTestComplete)
|
|
522
|
+
{
|
|
523
|
+
this.timeout(5000);
|
|
524
|
+
var tmpServer;
|
|
525
|
+
tmpServer = createHangingServer(function (pServer, pPort)
|
|
526
|
+
{
|
|
527
|
+
var testFable = new libFable();
|
|
528
|
+
var tmpRestClient = testFable.instantiateServiceProvider('RestClient', { RequestTimeout: 300 }, 'RestClient-LiveTimeout');
|
|
529
|
+
|
|
530
|
+
var tmpStart = Date.now();
|
|
531
|
+
tmpRestClient.getJSON('http://localhost:' + pPort + '/hangs',
|
|
532
|
+
function (pError, pResponse, pBody)
|
|
533
|
+
{
|
|
534
|
+
var tmpElapsed = Date.now() - tmpStart;
|
|
535
|
+
Expect(pError).to.be.an.instanceof(Error);
|
|
536
|
+
Expect(pError.message).to.contain('timed out');
|
|
537
|
+
// Guard against the Node 20+ ~5s globalAgent timeout
|
|
538
|
+
// silently beating our 300ms setting.
|
|
539
|
+
Expect(tmpElapsed).to.be.lessThan(2500);
|
|
540
|
+
pServer.close();
|
|
541
|
+
fTestComplete();
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
);
|
|
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
|
+
);
|
|
297
821
|
}
|
|
298
822
|
);
|
|
299
823
|
|