212 lines
9.6 KiB
JavaScript
212 lines
9.6 KiB
JavaScript
var _ = require('lodash'),
|
|
async = require('async'),
|
|
uuid = require('uuid'),
|
|
|
|
// These are functions which a request passes through _before_ being sent. They take care of stuff such as
|
|
// variable resolution, loading of files, etc.
|
|
prehelpers = require('../request-helpers-presend'),
|
|
|
|
// Similarly, these run after the request, and have the power to dictate whether a request should be re-queued
|
|
posthelpers = require('../request-helpers-postsend'),
|
|
|
|
ReplayController = require('../replay-controller'),
|
|
RequesterPool = require('../../requester').RequesterPool,
|
|
|
|
RESPONSE_START_EVENT_BASE = 'response.start.',
|
|
RESPONSE_END_EVENT_BASE = 'response.end.';
|
|
|
|
module.exports = {
|
|
init: function (done) {
|
|
// Request timeouts are applied by the requester, so add them to requester options (if any).
|
|
|
|
// create a requester pool
|
|
this.requester = new RequesterPool(this.options, done);
|
|
},
|
|
|
|
// the http trigger is actually directly triggered by the requester
|
|
// todo - figure out whether we should trigger it from here rather than the requester.
|
|
triggers: ['beforeRequest', 'request', 'responseStart', 'io'],
|
|
|
|
process: {
|
|
/**
|
|
* @param {Object} payload
|
|
* @param {Item} payload.item
|
|
* @param {Object} payload.data
|
|
* @param {Object} payload.context
|
|
* @param {VariableScope} payload.globals
|
|
* @param {VariableScope} payload.environment
|
|
* @param {Cursor} payload.coords
|
|
* @param {Boolean} payload.abortOnError
|
|
* @param {String} payload.source
|
|
* @param {Function} next
|
|
*
|
|
* @todo validate payload
|
|
*/
|
|
httprequest: function (payload, next) {
|
|
var abortOnError = _.has(payload, 'abortOnError') ? payload.abortOnError : this.options.abortOnError,
|
|
self = this,
|
|
context;
|
|
|
|
context = payload.context;
|
|
|
|
// generates a unique id for each http request
|
|
// a collection request can have multiple http requests
|
|
_.set(context, 'coords.httpRequestId', payload.httpRequestId || uuid());
|
|
|
|
// Run the helper functions
|
|
async.applyEachSeries(prehelpers, context, self, function (err) {
|
|
var xhr,
|
|
aborted,
|
|
item = context.item,
|
|
beforeRequest,
|
|
afterRequest,
|
|
safeNext;
|
|
|
|
// finish up current command
|
|
safeNext = function (error, finalPayload) {
|
|
// the error is passed twice to allow control between aborting the error vs just
|
|
// bubbling it up
|
|
return next((error && abortOnError) ? error : null, finalPayload, error);
|
|
};
|
|
|
|
// Helper function which calls the beforeRequest trigger ()
|
|
beforeRequest = function (err) {
|
|
self.triggers.beforeRequest(err, context.coords, item.request, payload.item, {
|
|
httpRequestId: context.coords && context.coords.httpRequestId,
|
|
abort: function () {
|
|
!aborted && xhr && xhr.abort();
|
|
aborted = true;
|
|
}
|
|
});
|
|
};
|
|
|
|
// Helper function to call the afterRequest trigger.
|
|
afterRequest = function (err, response, request, cookies, history) {
|
|
self.triggers.request(err, context.coords, response, request, payload.item, cookies, history);
|
|
};
|
|
|
|
// Ensure that this is called.
|
|
beforeRequest(null);
|
|
|
|
if (err) {
|
|
// Since we encountered an error before even attempting to send the request, we bubble it up
|
|
// here.
|
|
afterRequest(err, undefined, item.request);
|
|
|
|
return safeNext(
|
|
err,
|
|
{request: item.request, coords: context.coords, item: context.originalItem}
|
|
);
|
|
}
|
|
|
|
if (aborted) {
|
|
return next(new Error('runtime: request aborted'));
|
|
}
|
|
|
|
self.requester.create({
|
|
type: 'http',
|
|
source: payload.source,
|
|
cursor: context.coords
|
|
}, function (err, requester) {
|
|
if (err) { return next(err); } // this should never happen
|
|
|
|
var requestId = uuid(),
|
|
replayOptions;
|
|
|
|
// eslint-disable-next-line max-len
|
|
requester.on(RESPONSE_START_EVENT_BASE + requestId, function (err, response, request, cookies, history) {
|
|
// we could have also added the response to the set of responses in the cloned item,
|
|
// but then, we would have to iterate over all of them, which seems unnecessary
|
|
context.response = response;
|
|
|
|
// run the post request helpers, which need to use the response, assigned above
|
|
async.applyEachSeries(posthelpers, context, self, function (error, options) {
|
|
if (error) {
|
|
return;
|
|
}
|
|
|
|
// find the first helper that requested a replay
|
|
replayOptions = _.find(options, {replay: true});
|
|
|
|
// bail out if we know that request will be replayed.
|
|
if (replayOptions) {
|
|
return;
|
|
}
|
|
|
|
// bail out if its a pm.sendRequest
|
|
// @todo find a better way of identifying scripts
|
|
// @note don't use source='script'. Script requests
|
|
// can trigger `*.auth` source requests as well.
|
|
if (context.coords && context.coords.scriptId) {
|
|
return;
|
|
}
|
|
|
|
// trigger responseStart only for collection request.
|
|
// if there are replays, this will be triggered for the last request in the replay chain.
|
|
self.triggers.responseStart(err, context.coords, response, request, payload.item, cookies,
|
|
history);
|
|
});
|
|
});
|
|
|
|
requester.on(RESPONSE_END_EVENT_BASE + requestId, self.triggers.io.bind(self.triggers));
|
|
|
|
// eslint-disable-next-line max-len
|
|
xhr = requester.request(requestId, item.request, context.protocolProfileBehavior, function (err, res, req, cookies, history) {
|
|
err = err || null;
|
|
|
|
var nextPayload = {
|
|
response: res,
|
|
request: req,
|
|
item: context.originalItem,
|
|
cookies: cookies,
|
|
coords: context.coords,
|
|
history: history
|
|
},
|
|
replayController;
|
|
|
|
// trigger the request event.
|
|
// @note - we give the _original_ item in this trigger, so someone can do reference
|
|
// checking. Not sure if we should do that or not, but that's how it is.
|
|
// Don't break it.
|
|
afterRequest(err, res, req, cookies, history);
|
|
|
|
// Dispose off the requester, we don't need it anymore.
|
|
requester.dispose();
|
|
|
|
// do not process replays if there was an error
|
|
if (err) {
|
|
return safeNext(err, nextPayload);
|
|
}
|
|
|
|
// request replay logic
|
|
if (replayOptions) {
|
|
// prepare for replay
|
|
replayController = new ReplayController(context.replayState, self);
|
|
|
|
// replay controller invokes callback no. 1 when replaying the request
|
|
// invokes callback no. 2 when replay count has exceeded maximum limit
|
|
// @note: errors in replayed requests are passed to callback no. 1
|
|
return replayController.requestReplay(context,
|
|
context.item,
|
|
{source: replayOptions.helper},
|
|
// new payload with response from replay is sent to `next`
|
|
function (err, payloadFromReplay) { safeNext(err, payloadFromReplay); },
|
|
// replay was stopped, move on with older payload
|
|
function (err) {
|
|
// warn users that maximum retries have exceeded
|
|
// but don't bubble up the error with the request
|
|
self.triggers.console(context.coords, 'warn', (err.message || err));
|
|
safeNext(null, nextPayload);
|
|
}
|
|
);
|
|
}
|
|
|
|
// finish up for any other request
|
|
return safeNext(err, nextPayload);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|