UNPKG

node-request-interceptor

Version:

Low-level HTTP/HTTPS/XHR request interception library for NodeJS

401 lines 22.2 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createXMLHttpRequestOverride = void 0; /** * XMLHttpRequest override class. * Inspired by https://github.com/marvinhagemeister/xhr-mocklet. */ var until_1 = require("@open-draft/until"); var headers_utils_1 = require("headers-utils"); var parseJson_1 = require("../../utils/parseJson"); var createEvent_1 = require("./helpers/createEvent"); var createDebug = require('debug'); exports.createXMLHttpRequestOverride = function (middleware, context, XMLHttpRequestPristine) { var _a; var debug = createDebug('XHR'); return _a = /** @class */ (function () { function XMLHttpRequestOverride() { this.requestHeaders = {}; this.responseHeaders = {}; // Collection of events modified by `addEventListener`/`removeEventListener` calls. this._events = []; this.UNSENT = 0; this.OPENED = 1; this.HEADERS_RECEIVED = 2; this.LOADING = 3; this.DONE = 4; this.onreadystatechange = null; /* Events */ this.onabort = null; this.onerror = null; this.onload = null; this.onloadend = null; this.onloadstart = null; this.onprogress = null; this.ontimeout = null; this.url = ''; this.method = 'GET'; this.readyState = this.UNSENT; this.withCredentials = false; this.status = 200; this.statusText = 'OK'; this.data = ''; this.response = ''; this.responseType = 'text'; this.responseText = ''; this.responseXML = null; this.responseURL = ''; this.upload = null; this.timeout = 0; } XMLHttpRequestOverride.prototype.triggerReadyStateChange = function (options) { if (this.onreadystatechange) { this.onreadystatechange.call(this, createEvent_1.createEvent(options, this, 'readystatechange')); } }; XMLHttpRequestOverride.prototype.trigger = function (eventName, options) { debug('trigger', eventName); this.triggerReadyStateChange(options); var loadendEvent = this._events.find(function (event) { return event.name === 'loadend'; }); if (this.readyState === this.DONE && (this.onloadend || loadendEvent)) { var listener = this.onloadend || (loadendEvent === null || loadendEvent === void 0 ? void 0 : loadendEvent.listener); listener === null || listener === void 0 ? void 0 : listener.call(this, createEvent_1.createEvent(options, this, 'loadend')); } // Call the direct callback, if present. var directCallback = this["on" + eventName]; directCallback === null || directCallback === void 0 ? void 0 : directCallback.call(this, createEvent_1.createEvent(options, this, eventName)); // Check in the list of events attached via `addEventListener`. for (var _i = 0, _a = this._events; _i < _a.length; _i++) { var event_1 = _a[_i]; if (event_1.name === eventName) { event_1.listener.call(this, createEvent_1.createEvent(options, this, eventName)); } } return this; }; XMLHttpRequestOverride.prototype.reset = function () { debug('reset'); this.readyState = this.UNSENT; this.status = 200; this.statusText = ''; this.requestHeaders = {}; this.responseHeaders = {}; this.data = ''; this.response = null; this.responseText = null; this.responseXML = null; }; XMLHttpRequestOverride.prototype.open = function (method, url, async, user, password) { if (async === void 0) { async = true; } return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { debug = createDebug("XHR " + method + " " + url); debug('open', { method: method, url: url, async: async, user: user, password: password }); this.reset(); this.readyState = this.OPENED; if (typeof url === 'undefined') { this.url = method; this.method = 'GET'; } else { this.url = url; this.method = method; this.async = async; this.user = user; this.password = password; } return [2 /*return*/]; }); }); }; XMLHttpRequestOverride.prototype.send = function (data) { var _this = this; debug('send %s %s', this.method, this.url); this.readyState = this.LOADING; this.data = data || ''; var url; try { url = new URL(this.url); } catch (error) { // Assume a relative URL, if construction of a new `URL` instance fails. // Since `XMLHttpRequest` always executed in a DOM-like environment, // resolve the relative request URL against the current window location. url = new URL(this.url, window.location.href); } var requestHeaders = headers_utils_1.reduceHeadersObject(this.requestHeaders, function (headers, name, value) { headers[name.toLowerCase()] = value; return headers; }, {}); debug('request headers', requestHeaders); // Create an intercepted request instance exposed to the request intercepting middleware. var req = { url: url, method: this.method, body: this.data, headers: requestHeaders, }; debug('awaiting mocked response...'); Promise.resolve(until_1.until(function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, middleware(req, this)]; }); }); })).then(function (_a) { var _b; var middlewareException = _a[0], mockedResponse = _a[1]; // When the request middleware throws an exception, error the request. // This cancels the request and is similar to a network error. if (middlewareException) { debug('middleware function threw an exception!', middlewareException); // No way to propagate the actual error message. _this.trigger('error'); _this.abort(); return; } // Return a mocked response, if provided in the middleware. if (mockedResponse) { debug('received mocked response', mockedResponse); _this.status = mockedResponse.status || 200; _this.statusText = mockedResponse.statusText || 'OK'; _this.responseHeaders = mockedResponse.headers ? headers_utils_1.flattenHeadersObject(mockedResponse.headers) : {}; debug('assigned response status', _this.status, _this.statusText); debug('assigned response headers', _this.responseHeaders); // Mark that response headers has been received // and trigger a ready state event to reflect received headers // in a custom `onreadystatechange` callback. _this.readyState = _this.HEADERS_RECEIVED; _this.triggerReadyStateChange(); debug('response type', _this.responseType); _this.response = _this.getResponseBody(mockedResponse.body); _this.responseText = mockedResponse.body || ''; debug('assigned response body', _this.response); if (mockedResponse.body && _this.response) { // Presense of the mocked response implies a response body (not null). // Presece of the coerced `this.response` implies the mocked body is valid. var bodyBuffer = Buffer.from(mockedResponse.body); // Trigger a progress event based on the mocked response body. _this.trigger('progress', { loaded: bodyBuffer.length, total: bodyBuffer.length, }); } // Explicitly mark the request as done, so its response never hangs. // @see https://github.com/mswjs/node-request-interceptor/issues/13 _this.readyState = _this.DONE; _this.trigger('loadstart'); _this.trigger('load'); _this.trigger('loadend'); context.emitter.emit('response', req, { status: _this.status, statusText: _this.statusText, headers: mockedResponse.headers || {}, body: mockedResponse.body, }); } else { debug('no mocked response received'); // Perform an original request, when the request middleware returned no mocked response. var originalRequest_1 = new XMLHttpRequestPristine(); debug('opening an original request %s %s', _this.method, _this.url); originalRequest_1.open(_this.method, _this.url, (_b = _this.async) !== null && _b !== void 0 ? _b : true, _this.user, _this.password); // Reflect a successful state of the original request // on the patched instance. originalRequest_1.onload = function () { debug('original onload'); _this.status = originalRequest_1.status; _this.statusText = originalRequest_1.statusText; _this.responseURL = originalRequest_1.responseURL; _this.responseType = originalRequest_1.responseType; _this.response = originalRequest_1.response; _this.responseText = originalRequest_1.responseText; _this.responseXML = originalRequest_1.responseXML; debug('received original response status:', _this.status, _this.statusText); debug('received original response body:', _this.response); _this.trigger('loadstart'); _this.trigger('load'); var responseHeaders = originalRequest_1.getAllResponseHeaders(); _this.responseHeaders = headers_utils_1.flattenHeadersObject(headers_utils_1.headersToObject(headers_utils_1.stringToHeaders(responseHeaders))); debug('original response headers', responseHeaders); var normalizedResponseHeaders = headers_utils_1.headersToObject(headers_utils_1.stringToHeaders(responseHeaders)); debug('original response headers (normalized)', normalizedResponseHeaders); context.emitter.emit('response', req, { status: originalRequest_1.status, statusText: originalRequest_1.statusText, headers: normalizedResponseHeaders, body: originalRequest_1.response, }); }; // Assign callbacks and event listeners from the intercepted XHR instance // to the original XHR instance. _this.propagateCallbacks(originalRequest_1); _this.propagateListeners(originalRequest_1); _this.propagateHeaders(originalRequest_1, requestHeaders); if (_this.async) { originalRequest_1.timeout = _this.timeout; } debug('send', _this.data); originalRequest_1.send(_this.data); } }); }; XMLHttpRequestOverride.prototype.abort = function () { debug('abort'); if (this.readyState > this.UNSENT && this.readyState < this.DONE) { this.readyState = this.UNSENT; this.trigger('abort'); } }; XMLHttpRequestOverride.prototype.dispatchEvent = function () { return false; }; XMLHttpRequestOverride.prototype.setRequestHeader = function (name, value) { debug('set request header', name, value); this.requestHeaders[name] = value; }; XMLHttpRequestOverride.prototype.getResponseHeader = function (name) { debug('get response header', name); if (this.readyState < this.HEADERS_RECEIVED) { debug('cannot return a header: headers not received (state: %s)', this.readyState); return null; } var headerValue = Object.entries(this.responseHeaders).reduce(function (_, _a) { var headerName = _a[0], headerValue = _a[1]; // Ignore header name casing while still allowing to set response headers // with an arbitrary casing (no normalization). if ([headerName, headerName.toLowerCase()].includes(name)) { return headerValue; } return null; }, null); debug('resolved response header', name, headerValue, this.responseHeaders); return headerValue; }; XMLHttpRequestOverride.prototype.getAllResponseHeaders = function () { debug('get all response headers'); if (this.readyState < this.HEADERS_RECEIVED) { debug('cannot return headers: headers not received (state: %s)', this.readyState); return ''; } return Object.entries(this.responseHeaders) .map(function (_a) { var name = _a[0], value = _a[1]; return name + ": " + value + " \r\n"; }) .join(''); }; XMLHttpRequestOverride.prototype.addEventListener = function (name, listener) { debug('addEventListener', name, listener); this._events.push({ name: name, listener: listener, }); }; XMLHttpRequestOverride.prototype.removeEventListener = function (name, listener) { debug('removeEventListener', name, listener); this._events = this._events.filter(function (storedEvent) { return storedEvent.name !== name && storedEvent.listener !== listener; }); }; XMLHttpRequestOverride.prototype.overrideMimeType = function () { }; /** * Sets a proper `response` property based on the `responseType` value. */ XMLHttpRequestOverride.prototype.getResponseBody = function (body) { // Handle an improperly set "null" value of the mocked response body. var textBody = body !== null && body !== void 0 ? body : ''; debug('coerced response body to', textBody); switch (this.responseType) { case 'json': { debug('resolving response body as JSON'); return parseJson_1.parseJson(textBody); } case 'blob': { var blobType = this.getResponseHeader('content-type') || 'text/plain'; debug('resolving response body as Blob', { type: blobType }); return new Blob([textBody], { type: blobType, }); } case 'arraybuffer': { debug('resolving response body as ArrayBuffer'); var buffer = Buffer.from(textBody); var arrayBuffer = new Uint8Array(buffer); return arrayBuffer; } default: return textBody; } }; /** * Propagates captured XHR instance callbacks to the given XHR instance. * @note that `onload` listener is explicitly omitted. */ XMLHttpRequestOverride.prototype.propagateCallbacks = function (req) { req.onabort = this.abort; req.onerror = this.onerror; req.ontimeout = this.ontimeout; req.onloadstart = this.onloadstart; req.onloadend = this.onloadend; req.onprogress = this.onprogress; req.onreadystatechange = this.onreadystatechange; }; XMLHttpRequestOverride.prototype.propagateListeners = function (req) { this._events.forEach(function (_a) { var name = _a.name, listener = _a.listener; req.addEventListener(name, listener); }); }; XMLHttpRequestOverride.prototype.propagateHeaders = function (req, headers) { var flatHeaders = headers_utils_1.flattenHeadersObject(headers); Object.entries(flatHeaders).forEach(function (_a) { var key = _a[0], value = _a[1]; req.setRequestHeader(key, value); }); }; return XMLHttpRequestOverride; }()), /* Request state */ _a.UNSENT = 0, _a.OPENED = 1, _a.HEADERS_RECEIVED = 2, _a.LOADING = 3, _a.DONE = 4, _a; }; //# sourceMappingURL=XMLHttpRequestOverride.js.map