@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.
- package/.eslintrc +46 -0
- package/LICENSE.txt +353 -0
- package/README.md +1 -0
- package/index.js +75 -0
- package/lib/engine_http.js +886 -0
- package/lib/engine_socketio.js +376 -0
- package/lib/engine_ws.js +366 -0
- package/lib/is-idle-phase.js +14 -0
- package/lib/phases.js +184 -0
- package/lib/readers.js +65 -0
- package/lib/runner.js +454 -0
- package/lib/ssms.js +715 -0
- package/lib/weighted-pick.js +88 -0
- package/package.json +49 -0
|
@@ -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
|
+
}
|