@artilleryio/int-core 1.0.0

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.
@@ -0,0 +1,886 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+ 'use strict';
6
+
7
+ const async = require('async');
8
+ const _ = require('lodash');
9
+ const request = require('got');
10
+ const tough = require('tough-cookie');
11
+ const debug = require('debug')('http');
12
+ const debugRequests = require('debug')('http:request');
13
+ const debugResponse = require('debug')('http:response');
14
+ const USER_AGENT = 'Artillery (https://artillery.io)';
15
+ const engineUtil = require('@artilleryio/int-commons').engine_util;
16
+ const ensurePropertyIsAList = engineUtil.ensurePropertyIsAList;
17
+ const template = engineUtil.template;
18
+ const qs = require('querystring');
19
+ const filtrex = require('filtrex');
20
+ const urlparse = require('url').parse;
21
+ const FormData = require('form-data');
22
+ const HttpAgent = require('agentkeepalive');
23
+ const { HttpsAgent } = HttpAgent;
24
+ const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
25
+ const decompressResponse = require('decompress-response');
26
+
27
+ const { promisify } = require('node:util');
28
+
29
+ module.exports = HttpEngine;
30
+
31
+ const DEFAULT_AGENT_OPTIONS = {
32
+ keepAlive: true,
33
+ keepAliveMsec: 1000
34
+ };
35
+
36
+ function createAgents(proxies, opts) {
37
+ const agentOpts = Object.assign({}, DEFAULT_AGENT_OPTIONS, opts);
38
+
39
+ const result = {
40
+ httpAgent: null,
41
+ httpsAgent: null
42
+ };
43
+
44
+ // HTTP proxy endpoint will be used for all requests, unless a separate
45
+ // HTTPS proxy URL is also set, which will be used for HTTPS requests:
46
+ if (proxies.http) {
47
+ agentOpts.proxy = proxies.http;
48
+ result.httpAgent = new HttpProxyAgent(agentOpts);
49
+
50
+ if (proxies.https) {
51
+ agentOpts.proxy = proxies.https;
52
+ }
53
+
54
+ result.httpsAgent = new HttpsProxyAgent(agentOpts);
55
+ return result;
56
+ }
57
+
58
+ // If only HTTPS proxy is provided, it will be used for HTTPS requests,
59
+ // but not for HTTP requests:
60
+ if (proxies.https) {
61
+ result.httpAgent = new HttpAgent(agentOpts);
62
+ result.httpsAgent = new HttpsProxyAgent(
63
+ Object.assign({ proxy: proxies.https }, agentOpts)
64
+ );
65
+
66
+ return result;
67
+ }
68
+
69
+ // By default nothing is proxied:
70
+ result.httpAgent = new HttpAgent(agentOpts);
71
+ result.httpsAgent = new HttpsAgent(agentOpts);
72
+ return result;
73
+ }
74
+
75
+ function HttpEngine(script) {
76
+ this.config = script.config;
77
+
78
+ if (typeof this.config.defaults === 'undefined') {
79
+ this.config.defaults = {};
80
+ }
81
+
82
+ if (typeof this.config.http === 'undefined') {
83
+ this.config.http = {};
84
+ }
85
+
86
+ if(typeof this.config.http.cookieJarOptions === 'undefined') {
87
+ this.config.http.cookieJarOptions = {};
88
+ };
89
+
90
+ // If config.http.pool is set, create & reuse agents for all requests (with
91
+ // max sockets set). That's what we're done here.
92
+ // If config.http.pool is not set, we create new agents for each virtual user.
93
+ // That's done when the VU is initialized.
94
+
95
+ this.maxSockets = Infinity;
96
+ if (script.config.http && script.config.http.pool) {
97
+ this.maxSockets = Number(script.config.http.pool);
98
+ }
99
+ const agentOpts = Object.assign(DEFAULT_AGENT_OPTIONS, {
100
+ maxSockets: this.maxSockets,
101
+ maxFreeSockets: this.maxSockets
102
+ });
103
+
104
+ const agents = createAgents(
105
+ {
106
+ http: process.env.HTTP_PROXY,
107
+ https: process.env.HTTPS_PROXY
108
+ },
109
+ agentOpts
110
+ );
111
+
112
+ this._httpAgent = agents.httpAgent;
113
+ this._httpsAgent = agents.httpsAgent;
114
+
115
+ if (
116
+ (script.config.http && script.config.http.extendedMetrics === true) ||
117
+ global.artillery?.runtimeOptions.extendedHTTPMetrics
118
+ ) {
119
+ this.extendedHTTPMetrics = true;
120
+ }
121
+ }
122
+
123
+ HttpEngine.prototype.createScenario = function (scenarioSpec, ee) {
124
+ var self = this;
125
+
126
+ ensurePropertyIsAList(scenarioSpec, 'beforeRequest');
127
+ ensurePropertyIsAList(scenarioSpec, 'afterResponse');
128
+ ensurePropertyIsAList(scenarioSpec, 'beforeScenario');
129
+ ensurePropertyIsAList(scenarioSpec, 'afterScenario');
130
+ ensurePropertyIsAList(scenarioSpec, 'onError');
131
+
132
+ // Add scenario-level hooks if needed:
133
+ // For now, just turn them into function steps and insert them
134
+ // directly into the flow array.
135
+ // TODO: Scenario-level hooks will probably want access to the
136
+ // entire scenario spec rather than just the userContext.
137
+ const beforeScenarioFns = _.map(
138
+ scenarioSpec.beforeScenario,
139
+ function (hookFunctionName) {
140
+ return { function: hookFunctionName };
141
+ }
142
+ );
143
+ const afterScenarioFns = _.map(
144
+ scenarioSpec.afterScenario,
145
+ function (hookFunctionName) {
146
+ return { function: hookFunctionName };
147
+ }
148
+ );
149
+
150
+ const newFlow = beforeScenarioFns.concat(
151
+ scenarioSpec.flow.concat(afterScenarioFns)
152
+ );
153
+
154
+ scenarioSpec.flow = newFlow;
155
+
156
+ let tasks = _.map(scenarioSpec.flow, function (rs) {
157
+ return self.step(rs, ee, {
158
+ beforeRequest: scenarioSpec.beforeRequest,
159
+ afterResponse: scenarioSpec.afterResponse,
160
+ onError: scenarioSpec.onError
161
+ });
162
+ });
163
+
164
+ return self.compile(tasks, scenarioSpec.flow, ee);
165
+ };
166
+
167
+ HttpEngine.prototype.step = function step(requestSpec, ee, opts) {
168
+ opts = opts || {};
169
+ let self = this;
170
+ let config = this.config;
171
+
172
+ if (requestSpec.loop) {
173
+ let steps = _.map(requestSpec.loop, function (rs) {
174
+ return self.step(rs, ee, opts);
175
+ });
176
+
177
+ return engineUtil.createLoopWithCount(requestSpec.count || -1, steps, {
178
+ loopValue: requestSpec.loopValue || '$loopCount',
179
+ loopElement: requestSpec.loopElement || '$loopElement',
180
+ overValues: requestSpec.over,
181
+ whileTrue: self.config.processor
182
+ ? self.config.processor[requestSpec.whileTrue]
183
+ : undefined
184
+ });
185
+ }
186
+
187
+ if (requestSpec.parallel) {
188
+ let steps = _.map(requestSpec.parallel, function (rs) {
189
+ return self.step(rs, ee, opts);
190
+ });
191
+
192
+ return engineUtil.createParallel(steps, {
193
+ limitValue: requestSpec.limit
194
+ });
195
+ }
196
+
197
+ if (typeof requestSpec.think !== 'undefined') {
198
+ return engineUtil.createThink(requestSpec, self.config.defaults.think);
199
+ }
200
+
201
+ if (typeof requestSpec.log !== 'undefined') {
202
+ return function (context, callback) {
203
+ console.log(template(requestSpec.log, context));
204
+ return process.nextTick(function () {
205
+ callback(null, context);
206
+ });
207
+ };
208
+ }
209
+
210
+ if (requestSpec.function) {
211
+ return function (context, callback) {
212
+ let processFunc = self.config.processor[requestSpec.function];
213
+ if (processFunc) {
214
+ return processFunc(context, ee, function (hookErr) {
215
+ return callback(hookErr, context);
216
+ });
217
+ } else {
218
+ debug(`Function "${requestSpec.function}" not defined`);
219
+ debug('processor: %o', self.config.processor);
220
+ ee.emit('error', `Undefined function "${requestSpec.function}"`);
221
+ return process.nextTick(function () {
222
+ callback(null, context);
223
+ });
224
+ }
225
+ };
226
+ }
227
+
228
+ let f = function (context, callback) {
229
+ let method = _.keys(requestSpec)[0].toUpperCase();
230
+ let params = requestSpec[method.toLowerCase()];
231
+
232
+ const onErrorHandlers = opts.onError; // only scenario-lever onError handlers are supported
233
+
234
+ // A special case for when "url" attribute is missing. We need to check for
235
+ // it manually as request.js won't emit an 'error' event when the argument
236
+ // is missing.
237
+ // This will be obsoleted by better script validation.
238
+ if (!params.url && !params.uri) {
239
+ let err = new Error('an URL must be specified');
240
+ return callback(err, context);
241
+ }
242
+
243
+ let tls = config.tls || {};
244
+ let timeout = config.timeout || _.get(config, 'http.timeout') || 10;
245
+
246
+ if (!engineUtil.isProbableEnough(params)) {
247
+ return process.nextTick(function () {
248
+ callback(null, context);
249
+ });
250
+ }
251
+
252
+ if (!_.isUndefined(params.ifTrue)) {
253
+ let result;
254
+ try {
255
+ const cond = _.has(config.processor, params.ifTrue)
256
+ ? config.processor[params.ifTrue]
257
+ : filtrex(params.ifTrue);
258
+ result = cond(context.vars);
259
+ } catch (err) {
260
+ debug('ifTrue error:', err);
261
+ result = 1; // if the expression is incorrect, just proceed
262
+ }
263
+ if (!result) {
264
+ return process.nextTick(function () {
265
+ callback(null, context);
266
+ });
267
+ }
268
+ }
269
+
270
+ // Run beforeRequest processors (scenario-level ones too)
271
+ const requestParams = _.extend(_.clone(params), {
272
+ url: maybePrependBase(params.url || params.uri, config), // *NOT* templating here
273
+ method: method,
274
+ timeout: timeout * 1000
275
+ });
276
+
277
+ if (context._enableCookieJar) {
278
+ requestParams.cookieJar = context._jar;
279
+ }
280
+
281
+ if (tls) {
282
+ requestParams.https = requestParams.https || {};
283
+ requestParams.https = _.extend(requestParams.https, tls);
284
+ }
285
+
286
+ let functionNames = _.concat(
287
+ opts.beforeRequest || [],
288
+ params.beforeRequest || []
289
+ );
290
+
291
+ async.eachSeries(
292
+ functionNames,
293
+ function iteratee(functionName, next) {
294
+ let fn = template(functionName, context);
295
+ let processFunc = config.processor[fn];
296
+ if (!processFunc) {
297
+ processFunc = function (r, c, e, cb) {
298
+ return cb(null);
299
+ };
300
+ console.log(`WARNING: custom function ${fn} could not be found`); // TODO: a 'warning' event
301
+ }
302
+
303
+ processFunc(requestParams, context, ee, function (err) {
304
+ if (err) {
305
+ return next(err);
306
+ }
307
+ return next(null);
308
+ });
309
+ },
310
+ function done(err) {
311
+ if (err) {
312
+ debug(err);
313
+ return callback(err, context);
314
+ }
315
+
316
+ // Order of precedence: json set in a function, json set in the script, body set in a function, body set in the script.
317
+ if (requestParams.json) {
318
+ requestParams.json = template(requestParams.json, context);
319
+ delete requestParams.body;
320
+ } else if (requestParams.body) {
321
+ requestParams.body = template(requestParams.body, context);
322
+ // TODO: Warn if body is not a string or a buffer
323
+ }
324
+
325
+ // add loop, name & uri elements to be interpolated
326
+ if (context.vars.$loopElement) {
327
+ context.vars.$loopElement = template(
328
+ context.vars.$loopElement,
329
+ context
330
+ );
331
+ }
332
+ if (requestParams.name) {
333
+ requestParams.name = template(requestParams.name, context);
334
+ }
335
+ if (requestParams.uri) {
336
+ requestParams.uri = template(requestParams.uri, context);
337
+ }
338
+ if (requestParams.url) {
339
+ requestParams.url = template(requestParams.url, context);
340
+ }
341
+
342
+ // Follow all redirects by default unless specified otherwise
343
+ if (typeof requestParams.followRedirect === 'undefined') {
344
+ requestParams.followRedirect = true;
345
+ }
346
+
347
+ // TODO: Use traverse on the entire flow instead
348
+
349
+ // Request.js -> Got.js translation
350
+ if (params.qs) {
351
+ requestParams.searchParams = template(params.qs, context);
352
+ }
353
+ if (typeof params.gzip === 'boolean') {
354
+ requestParams.decompress = params.gzip;
355
+ } else {
356
+ requestParams.decompress = true;
357
+ }
358
+
359
+ if (params.form) {
360
+ requestParams.form = _.reduce(
361
+ requestParams.form,
362
+ function (acc, v, k) {
363
+ acc[k] = template(v, context);
364
+ return acc;
365
+ },
366
+ {}
367
+ );
368
+ }
369
+
370
+ if (params.formData) {
371
+ const f = new FormData();
372
+ requestParams.body = _.reduce(
373
+ requestParams.formData,
374
+ function (acc, v, k) {
375
+ // acc[k] = template(v, context);
376
+ acc.append(k, template(v, context));
377
+ return acc;
378
+ },
379
+ f
380
+ );
381
+ }
382
+
383
+ // Assign default headers then overwrite as needed
384
+ let defaultHeaders = lowcaseKeys(
385
+ config.defaults.headers || { 'user-agent': USER_AGENT }
386
+ );
387
+ const combinedHeaders = _.extend(
388
+ defaultHeaders,
389
+ lowcaseKeys(params.headers),
390
+ lowcaseKeys(requestParams.headers)
391
+ );
392
+ const templatedHeaders = _.mapValues(
393
+ combinedHeaders,
394
+ function (v, k, obj) {
395
+ return template(v, context);
396
+ }
397
+ );
398
+ requestParams.headers = templatedHeaders;
399
+
400
+ if (
401
+ typeof requestParams.cookie === 'object' ||
402
+ typeof context._defaultCookie === 'object'
403
+ ) {
404
+ requestParams.cookieJar = context._jar;
405
+ const cookie = Object.assign(
406
+ {},
407
+ context._defaultCookie,
408
+ requestParams.cookie
409
+ );
410
+ Object.keys(cookie).forEach(function (k) {
411
+ context._jar.setCookieSync(
412
+ k + '=' + template(cookie[k], context),
413
+ requestParams.url
414
+ );
415
+ });
416
+ }
417
+
418
+ if (typeof requestParams.auth === 'object') {
419
+ requestParams.username = template(requestParams.auth.user, context);
420
+ requestParams.password = template(requestParams.auth.pass, context);
421
+ delete requestParams.auth;
422
+ }
423
+
424
+ let url = maybePrependBase(
425
+ template(requestParams.uri || requestParams.url, context),
426
+ config
427
+ );
428
+
429
+ if (requestParams.uri) {
430
+ // If a hook function sets requestParams.uri to something, request.js
431
+ // will pick that over .url, so we need to delete it.
432
+ delete requestParams.uri;
433
+ }
434
+
435
+ requestParams.url = url;
436
+
437
+ // TODO: Bypass proxy if "proxy: false" is set
438
+ requestParams.agent = {
439
+ http: context._httpAgent,
440
+ https: context._httpsAgent
441
+ };
442
+
443
+ requestParams.throwHttpErrors = false;
444
+
445
+ if (!requestParams.url || !requestParams.url.startsWith('http')) {
446
+ let err = new Error(`Invalid URL - ${requestParams.url}`);
447
+ return callback(err, context);
448
+ }
449
+
450
+ function responseProcessor(isLast, res, body, done) {
451
+ if (process.env.DEBUG) {
452
+ let requestInfo = {
453
+ url: requestParams.url,
454
+ method: requestParams.method,
455
+ headers: requestParams.headers
456
+ };
457
+
458
+ if (
459
+ context._jar._jar &&
460
+ typeof context._jar._jar.getCookieStringSync === 'function'
461
+ ) {
462
+ requestInfo = Object.assign(requestInfo, {
463
+ cookie: context._jar._jar.getCookieStringSync(requestParams.url)
464
+ });
465
+ }
466
+
467
+ if (requestParams.json && typeof requestParams.json !== 'boolean') {
468
+ requestInfo.json = requestParams.json;
469
+ }
470
+
471
+ // If "json" is set to an object, it will be serialised and sent as body and the value of the "body" attribute will be ignored.
472
+ if (requestParams.body && typeof requestParams.json !== 'object') {
473
+ if (process.env.DEBUG.indexOf('http:full_body') > -1) {
474
+ // Show the entire body
475
+ requestInfo.body = requestParams.body;
476
+ } else {
477
+ // Only show the beginning of long bodies
478
+ if (typeof requestParams.body === 'string') {
479
+ requestInfo.body = requestParams.body.substring(0, 512);
480
+ if (requestParams.body.length > 512) {
481
+ requestInfo.body += ' ...';
482
+ }
483
+ } else if (typeof requestParams.body === 'object') {
484
+ requestInfo.body = `< ${requestParams.body.constructor.name} >`;
485
+ } else {
486
+ requestInfo.body = String(requestInfo.body);
487
+ }
488
+ }
489
+ }
490
+
491
+ if (requestParams.qs) {
492
+ requestInfo.qs = qs.encode(
493
+ Object.assign(
494
+ qs.parse(urlparse(requestParams.url).query),
495
+ requestParams.qs
496
+ )
497
+ );
498
+ }
499
+
500
+ debug('request: %s', JSON.stringify(requestInfo, null, 2));
501
+ }
502
+
503
+ debugResponse(JSON.stringify(res.headers, null, 2));
504
+ debugResponse(JSON.stringify(body, null, 2));
505
+
506
+ // capture/match/response hooks run only for last request in a task
507
+ if(!isLast) {
508
+ return done(null, context);
509
+ }
510
+
511
+ const resForCapture = { headers: res.headers, body: body };
512
+
513
+ engineUtil.captureOrMatch(
514
+ params,
515
+ resForCapture,
516
+ context,
517
+ function captured(err, result) {
518
+ if (err) {
519
+ // Run onError hooks and end the scenario:
520
+ runOnErrorHooks(
521
+ onErrorHandlers,
522
+ config.processor,
523
+ err,
524
+ requestParams,
525
+ context,
526
+ ee,
527
+ function (asyncErr) {
528
+ return done(err, context);
529
+ }
530
+ );
531
+ }
532
+
533
+ let haveFailedMatches = false;
534
+ let haveFailedCaptures = false;
535
+
536
+ if (result !== null) {
537
+ if (
538
+ Object.keys(result.matches).length > 0 ||
539
+ Object.keys(result.captures).length > 0
540
+ ) {
541
+ debug('captures and matches:');
542
+ debug(result.matches);
543
+ debug(result.captures);
544
+ }
545
+
546
+ // match and capture are strict by default:
547
+ haveFailedMatches = _.some(result.matches, function (v, k) {
548
+ return !v.success && v.strict !== false;
549
+ });
550
+
551
+ haveFailedCaptures = _.some(result.captures, function (v, k) {
552
+ return v.failed;
553
+ });
554
+
555
+ if (haveFailedMatches || haveFailedCaptures) {
556
+ // TODO: Emit the details of each failed capture/match
557
+ } else {
558
+ _.each(result.matches, function (v, k) {
559
+ ee.emit('match', v.success, {
560
+ expected: v.expected,
561
+ got: v.got,
562
+ expression: v.expression,
563
+ strict: v.strict
564
+ });
565
+ });
566
+
567
+ _.each(result.captures, function (v, k) {
568
+ _.set(context.vars, k, v.value);
569
+ });
570
+ }
571
+ }
572
+
573
+ // Now run afterResponse processors
574
+ let functionNames = _.concat(
575
+ opts.afterResponse || [],
576
+ params.afterResponse || []
577
+ );
578
+ async.eachSeries(
579
+ functionNames,
580
+ function iteratee(functionName, next) {
581
+ let fn = template(functionName, context);
582
+ let processFunc = config.processor[fn];
583
+ if (!processFunc) {
584
+ // TODO: DRY - #223
585
+ processFunc = function (r, res, c, e, cb) {
586
+ return cb(null);
587
+ };
588
+ console.log(
589
+ `WARNING: custom function ${fn} could not be found`
590
+ ); // TODO: a 'warning' event
591
+ }
592
+
593
+ // Got does not have res.body which Request.js used to have, so we attach it here:
594
+ res.body = body;
595
+
596
+ processFunc(requestParams, res, context, ee, function (err) {
597
+ if (err) {
598
+ return next(err);
599
+ }
600
+ return next(null);
601
+ });
602
+ },
603
+ function (err) {
604
+ if (err) {
605
+ debug(err);
606
+ return done(err, context);
607
+ }
608
+
609
+ if (haveFailedMatches || haveFailedCaptures) {
610
+ // FIXME: This means only one error in the report even if multiple captures failed for the same request.
611
+ return done(
612
+ new Error('Failed capture or match'),
613
+ context
614
+ );
615
+ }
616
+ return done(null, context);
617
+ }
618
+ );
619
+ }
620
+ );
621
+ }
622
+
623
+ let needToProcessResponse = false;
624
+ if (
625
+ typeof requestParams.capture === 'object' ||
626
+ typeof requestParams.match === 'object' ||
627
+ requestParams.afterResponse ||
628
+ (typeof opts.afterResponse === 'object' &&
629
+ opts.afterResponse.length > 0) ||
630
+ process.env.DEBUG
631
+ ) {
632
+ needToProcessResponse = true;
633
+ }
634
+
635
+ if (!requestParams.url) {
636
+ let err = new Error('an URL must be specified');
637
+
638
+ // Run onError hooks and end the scenario
639
+ runOnErrorHooks(
640
+ onErrorHandlers,
641
+ config.processor,
642
+ err,
643
+ requestParams,
644
+ context,
645
+ ee,
646
+ function (asyncErr) {
647
+ return callback(err, context);
648
+ }
649
+ );
650
+ }
651
+
652
+ requestParams.retry = 0; // disable retries - ignored when using streams
653
+
654
+ request(requestParams)
655
+ .on('request', function (req) {
656
+ debugRequests('request start: %s', req.path);
657
+ ee.emit('counter', 'http.requests', 1);
658
+ ee.emit('rate', 'http.request_rate');
659
+ req.on('response', function (res) {
660
+
661
+ self._handleResponse(
662
+ requestParams,
663
+ res,
664
+ ee,
665
+ context,
666
+ needToProcessResponse ? responseProcessor : null,
667
+ callback
668
+ );
669
+ });
670
+
671
+ })
672
+ .on('error', function (err, body, res) {
673
+ if (err.name === 'HTTPError') {
674
+ return;
675
+ }
676
+ // this is an ENOTFOUND, ECONNRESET etc
677
+ debug(err);
678
+ // Run onError hooks and end the scenario:
679
+ runOnErrorHooks(
680
+ onErrorHandlers,
681
+ config.processor,
682
+ err,
683
+ requestParams,
684
+ context,
685
+ ee,
686
+ function (asyncErr) {
687
+ return callback(err, context);
688
+ }
689
+ );
690
+ })
691
+ .catch((gotErr) => {
692
+ // TODO: Handle the error properly with run hooks
693
+ return callback(gotErr, context);
694
+ });
695
+ }
696
+ ); // eachSeries
697
+ };
698
+
699
+ return f;
700
+ };
701
+
702
+ HttpEngine.prototype._handleResponse = function (
703
+ requestParams,
704
+ res,
705
+ ee,
706
+ context,
707
+ responseProcessor,
708
+ callback
709
+ ) {
710
+ const url = requestParams.url;
711
+
712
+ if (requestParams.decompress) {
713
+ res = decompressResponse(res);
714
+ }
715
+
716
+ let code = res.statusCode;
717
+ if (!context._enableCookieJar) {
718
+ const rawCookies = res.headers['set-cookie'];
719
+ if (rawCookies) {
720
+ context._enableCookieJar = true;
721
+ rawCookies.forEach(function (cookieString) {
722
+ try {
723
+ context._jar.setCookieSync(cookieString, url);
724
+ } catch (err) {
725
+ debug(`Could not parse cookieString "${cookieString}" from response header, skipping it`);
726
+ debug(err);
727
+ ee.emit('error', 'cookie_parse_error_invalid_cookie');
728
+ }
729
+ });
730
+ }
731
+ }
732
+
733
+ ee.emit('counter', 'http.codes.' + code, 1);
734
+ ee.emit('counter', 'http.responses', 1);
735
+ // ee.emit('rate', 'http.response_rate');
736
+ ee.emit('histogram', 'http.response_time', res.timings.phases.firstByte);
737
+ if (this.extendedHTTPMetrics) {
738
+ ee.emit('histogram', 'http.dns', res.timings.phases.dns);
739
+ ee.emit('histogram', 'http.tcp', res.timings.phases.tcp);
740
+ ee.emit('histogram', 'http.tls', res.timings.phases.tls);
741
+ }
742
+ let body = '';
743
+ if (responseProcessor) {
744
+ res.on('data', (d) => {
745
+ body += d;
746
+ });
747
+ }
748
+
749
+ res.on('end', () => {
750
+ if (this.extendedHTTPMetrics) {
751
+ ee.emit('histogram', 'http.total', res.timings.phases.total);
752
+ }
753
+
754
+ context._successCount++;
755
+
756
+ // config.defaults won't be taken into account for this
757
+ const isLastRequest = lastRequest(res, requestParams);
758
+
759
+ if (responseProcessor) {
760
+ responseProcessor(isLastRequest, res, body, (processResponseErr) => {
761
+ // capture/match returned an error object, or a hook function returned
762
+ // with an error
763
+ if (processResponseErr) {
764
+ return callback(processResponseErr, context);
765
+ }
766
+
767
+ if (isLastRequest) {
768
+ return callback(null, context);
769
+ }
770
+ });
771
+ } else {
772
+ if (isLastRequest) {
773
+ return callback(null, context);
774
+ }
775
+ }
776
+ });
777
+ };
778
+
779
+ function lastRequest(res, requestParams) {
780
+ // We're done when:
781
+ // - 3xx response and not following redirects
782
+ // - not a 3xx response
783
+
784
+ return ((res.statusCode >= 300 && res.statusCode < 400 && !requestParams.followRedirect) ||
785
+ (res.statusCode < 300 || res.statusCode >= 400))
786
+ }
787
+
788
+ HttpEngine.prototype.setInitialContext = function (initialContext) {
789
+ initialContext._successCount = 0;
790
+
791
+ initialContext._defaultStrictCapture = this.config.defaults.strictCapture;
792
+ initialContext._jar = new tough.CookieJar(null, this.config.http.cookieJarOptions);
793
+
794
+ initialContext._enableCookieJar = false;
795
+ // If a default cookie is set, we will use the jar straightaway:
796
+ if (typeof this.config.defaults.cookie === 'object') {
797
+ initialContext._defaultCookie = this.config.defaults.cookie;
798
+ initialContext._enableCookieJar = true;
799
+ }
800
+
801
+ if (this.config.http && typeof this.config.http.pool !== 'undefined') {
802
+ // Reuse common agents (created in the engine instance constructor)
803
+ initialContext._httpAgent = this._httpAgent;
804
+ initialContext._httpsAgent = this._httpsAgent;
805
+ } else {
806
+ // Create agents just for this VU
807
+ const agentOpts = Object.assign(DEFAULT_AGENT_OPTIONS, {
808
+ maxSockets: 1,
809
+ maxFreeSockets: 1
810
+ });
811
+
812
+ const agents = createAgents(
813
+ {
814
+ http: process.env.HTTP_PROXY,
815
+ https: process.env.HTTPS_PROXY
816
+ },
817
+ agentOpts
818
+ );
819
+
820
+ initialContext._httpAgent = agents.httpAgent;
821
+ initialContext._httpsAgent = agents.httpsAgent;
822
+ }
823
+ return initialContext;
824
+ };
825
+
826
+ HttpEngine.prototype.compile = function compile(tasks, scenarioSpec, ee) {
827
+ let self = this;
828
+
829
+ return async function scenario(initialContext, callback) {
830
+ initialContext = self.setInitialContext(initialContext);
831
+ ee.emit('started');
832
+ let context = initialContext;
833
+ for (const task of tasks) {
834
+ try {
835
+ context = await promisify(task)(context);
836
+ } catch (taskErr) {
837
+ ee.emit('error', taskErr.code || taskErr.message);
838
+ return callback(taskErr, context); // calling back for now for existing client code
839
+ }
840
+ }
841
+ return callback(null, context);
842
+ };
843
+ };
844
+
845
+ function maybePrependBase(uri, config) {
846
+ if (_.startsWith(uri, '/')) {
847
+ return config.target + uri;
848
+ } else {
849
+ return uri;
850
+ }
851
+ }
852
+
853
+ /*
854
+ * Given a dictionary, return a dictionary with all keys lowercased.
855
+ */
856
+ function lowcaseKeys(h) {
857
+ return _.transform(h, function (result, v, k) {
858
+ result[k.toLowerCase()] = v;
859
+ });
860
+ }
861
+
862
+ function runOnErrorHooks(
863
+ functionNames,
864
+ functions,
865
+ err,
866
+ requestParams,
867
+ context,
868
+ ee,
869
+ callback
870
+ ) {
871
+ async.eachSeries(
872
+ functionNames,
873
+ function iteratee(functionName, next) {
874
+ let processFunc = functions[functionName];
875
+ processFunc(err, requestParams, context, ee, function (asyncErr) {
876
+ if (asyncErr) {
877
+ return next(asyncErr);
878
+ }
879
+ return next(null);
880
+ });
881
+ },
882
+ function done(asyncErr) {
883
+ return callback(asyncErr);
884
+ }
885
+ );
886
+ }