faux-jax-tulios
Version:
Fork using latest mitm for node 10 - Intercept and respond to requests in the browser (XMLHttpRequest, XDomainRequest) and Node.js (http(s) module)
418 lines (328 loc) • 11 kB
JavaScript
module.exports = XMLHttpRequest;
var assign = require('lodash-compat/object/assign');
var inherits = require('util').inherits;
var Event = require('../Event');
var EventTarget = require('../EventTarget')();
var events = require('./events');
var forbiddenHeaderNames = require('./forbidden-header-names');
var forbiddenMethods = require('./forbidden-methods');
var httpStatusCodes = require('./http-status-codes');
var native = require('../native');
var methods = require('./methods');
var states = require('./states');
var support = require('../support');
// https://xhr.spec.whatwg.org/#constructors
function XMLHttpRequest() {
EventTarget.call(this, events);
this.readyState;
if (support.xhr.timeout) {
this.timeout = 0;
}
if (support.xhr.cors) {
this.withCredentials = false;
}
this.upload;
this.responseType = '';
this.responseText = '';
this.responseXML = null;
this.readyState = states.UNSENT;
if (support.xhr.response) {
this.response = '';
}
if (support.xhr.responseURL) {
this.responseURL = '';
}
this.status = 0;
this.statusText = '';
}
inherits(XMLHttpRequest, EventTarget);
// https://xhr.spec.whatwg.org/#the-open()-method
XMLHttpRequest.prototype.open = function(method, url, async, username, password) {
var indexOf = require('lodash-compat/array/indexOf');
if (typeof method !== 'string') {
throw new SyntaxError('Invalid method');
}
var normalizedMethod = method.toUpperCase();
if (indexOf(methods, normalizedMethod) === -1 &&
indexOf(forbiddenMethods, normalizedMethod) !== -1 &&
method !== normalizedMethod) {
throw new Error('SecurityError');
}
if (indexOf(methods, normalizedMethod) === -1 &&
indexOf(forbiddenMethods, method) === -1) {
throw new SyntaxError('Invalid method');
}
// fauxJax's specific API
this.requestMethod = normalizedMethod;
this.requestURL = url;
if (typeof async === 'boolean') {
this.async = async;
} else {
this.async = true;
}
// we keep separate stores for headers,
// headers are case insensitive (http://tools.ietf.org/html/rfc7230#section-3.2)
// but current browser implementations will not normalize them over the wire, we mimic this
// 1. know if a particular header is set, get his provided name from his lowercased name
this._headerNames = {};
// 2. stores the provided header => value
this.requestHeaders = {};
this.username = username;
this.password = password;
readyStateChange(this, states.OPENED);
};
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-setrequestheader
XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
var indexOf = require('lodash-compat/array/indexOf');
if (this.readyState !== states.OPENED) {
throw new Error('InvalidStateError');
}
if (this.sendFlag === true) {
throw new Error('InvalidStateError');
}
if (!name || value === undefined) {
throw new SyntaxError('Missing name or value');
}
if (indexOf(forbiddenHeaderNames, name) !== -1) {
throw new Error('Refused to set unsafe header "' + name + '"');
}
var lowerCasedHeaderName = name.toLowerCase();
// only register a header once, first call sets the case
if (this._headerNames[lowerCasedHeaderName] === undefined) {
this._headerNames[lowerCasedHeaderName] = name;
}
var originalHeaderNameCase = this._headerNames[lowerCasedHeaderName];
if (this.requestHeaders[originalHeaderNameCase] === undefined) {
this.requestHeaders[originalHeaderNameCase] = value;
} else {
this.requestHeaders[originalHeaderNameCase] = this.requestHeaders[originalHeaderNameCase] + ', ' + value;
}
};
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-send
XMLHttpRequest.prototype.send = function(body) {
if (this.readyState !== states.OPENED) {
throw new Error('InvalidStateError');
}
if (this.sendFlag === true) {
throw new Error('InvalidStateError');
}
if (support.xhr.timeout) {
this._setTimeoutIfNecessary();
}
if (this.requestMethod === 'GET' || this.requestMethod === 'HEAD') {
this.requestBody = null;
} else {
this.requestBody = body;
}
if (this.requestBody && this._headerNames['content-type'] === undefined) {
this.requestHeaders['Content-Type'] = 'text-plain;charset=UTF-8';
}
this.sendFlag = true;
if (support.xhr.events.loadstart) {
dispatchProgressEvent(this, {
type: 'loadstart',
total: 0,
loaded: 0,
lengthComputable: false
});
}
};
// https://xhr.spec.whatwg.org/#the-abort()-method
XMLHttpRequest.prototype.abort = function() {
if (this.readyState > states.UNSENT && this.sendFlag === true) {
if (support.xhr.response) {
this.response = new Error('NetworkError');
}
handleRequestError(this, 'abort');
return;
}
this.responseType = '';
this.responseText = '';
this.responseXML = null;
this.readyState = states.UNSENT;
if (support.xhr.response) {
this.response = '';
}
if (support.xhr.responseURL) {
this.responseURL = '';
}
this.status = 0;
this.statusText = '';
if (support.xhr.timeout) {
this.timeout = 0;
}
};
// https://xhr.spec.whatwg.org/#the-getresponseheader()-method
XMLHttpRequest.prototype.getResponseHeader = function(headerName) {
headerName = headerName.toLowerCase();
for (var responseHeaderName in this.responseHeaders) {
if (responseHeaderName.toLowerCase() === headerName) {
return this.responseHeaders[responseHeaderName];
}
}
return null;
};
if (support.xhr.getAllResponseHeaders) {
// https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method
XMLHttpRequest.prototype.getAllResponseHeaders = function() {
var reduce = require('lodash-compat/collection/reduce');
if (!this.responseHeaders) {
return '';
}
function formatHeader(headers, headerValue, headerName) {
return headers + headerName + ': ' + headerValue + '\r\n';
}
return reduce(this.responseHeaders, formatHeader, '');
};
}
// now onto fauxJax's specific API
XMLHttpRequest.prototype.setResponseHeaders = function(headers) {
if (this.readyState === states.DONE) {
return;
}
var clone = require('lodash-compat/lang/clone');
if (!headers) {
throw new Error('Please specify at least one header when using xhr.setResponseHeaders()');
}
if (this.readyState < states.OPENED || this.sendFlag !== true) {
throw new Error('Call xhr.open() and xhr.send() before using xhr.setResponseHeaders()');
}
this.responseHeaders = clone(headers);
readyStateChange(this, states.HEADERS_RECEIVED);
};
XMLHttpRequest.prototype.setResponseBody = function(body) {
if (this.readyState === states.DONE) {
return;
}
var xhr = this;
var forEach = require('lodash-compat/collection/forEach');
if (typeof body !== 'string') {
throw new Error('xhr.setResponseBody() expects a String');
}
if (this.readyState < states.HEADERS_RECEIVED || this.sendFlag !== true) {
throw new Error('Call xhr.open(), xhr.send() and xhr.setResponseHeaders() before using xhr.setResponseBody()');
}
var lengthComputable = this.getResponseHeader('Content-Length') === undefined ? false : true;
var chunkSize = 10;
var index = 0;
while (index < body.length) {
this.responseText += body.slice(index, index + chunkSize);
index += chunkSize;
readyStateChange(this, states.LOADING);
if (support.xhr.events.progress) {
dispatchProgressEvent(this, {
type: 'progress',
total: this.getResponseHeader('Content-Length') || 0,
loaded: index,
lengthComputable: lengthComputable
});
}
}
if (support.xhr.response) {
if (this.responseType === 'json') {
this.response = JSON.parse(this.responseText);
} else {
this.response = this.responseText;
}
}
if (support.xhr.responseURL) {
var url = require('url');
// https://xhr.spec.whatwg.org/#the-responseurl-attribute
var responseURL = url.parse(url.resolve(location.href, this.requestURL));
delete responseURL.hash;
this.responseURL = url.format(responseURL);
}
if (this._timeoutID) {
clearTimeout(this._timeoutID);
}
readyStateChange(this, states.DONE);
forEach(['progress', 'load', 'loadend'], function progress(eventName) {
if (support.xhr.events[eventName]) {
dispatchProgressEvent(xhr, {
type: eventName,
total: xhr.getResponseHeader('Content-Length') || 0,
loaded: index,
lengthComputable: lengthComputable
});
}
});
};
XMLHttpRequest.prototype.respond = function(statusCode, headers, body) {
if (this.readyState === states.DONE) {
return;
}
this.status = statusCode;
this.statusText = httpStatusCodes[this.status];
if (headers) {
this.setResponseHeaders(headers);
}
if (body !== undefined) {
this.setResponseBody(body);
}
};
XMLHttpRequest.prototype._setTimeoutIfNecessary = function() {
var bind = require('lodash-compat/function/bind');
if (this.timeout > 0 && this._timeoutID === undefined) {
this._timeoutID = setTimeout(bind(handleRequestError, null, this, 'timeout'), this.timeout);
}
};
// set XMLHttpRequest.UNSENT etc. like browsers does it
// console.log(XMLHttpRequest.UNSENT), console.log(XMLHttpRequest.prototype.UNSENT)
if (native.XMLHttpRequest && native.XMLHttpRequest.OPENED) {
assign(XMLHttpRequest, states);
assign(XMLHttpRequest.prototype, states);
}
function dispatchEvent(eventTarget, type, params) {
params = params || {};
var event = new Event(type, {
bubbles: params.bubbles,
cancelable: params.cancelable
});
event.target = eventTarget;
event.currentTarget = eventTarget;
assign(event, params);
eventTarget.dispatchEvent(event);
}
function readyStateChange(eventTarget, readyState) {
eventTarget.readyState = readyState;
dispatchEvent(eventTarget, 'readystatechange', {
bubbles: false,
cancelable: false
});
}
// https://xhr.spec.whatwg.org/#handle-errors
// https://xhr.spec.whatwg.org/#request-error-steps
function handleRequestError(eventTarget, type) {
readyStateChange(eventTarget, states.DONE);
dispatchProgressEvent(eventTarget, {
type: 'progress',
total: 0,
loaded: 0,
lengthComputable: false
});
dispatchProgressEvent(eventTarget, {
type: type,
total: 0,
loaded: 0,
lengthComputable: false
});
dispatchProgressEvent(eventTarget, {
type: 'loadend',
total: 0,
loaded: 0,
lengthComputable: false
});
}
function dispatchProgressEvent(eventTarget, progressParams) {
dispatchEvent(eventTarget, progressParams.type, {
// deprecated https://codereview.chromium.org/492213004
totalSize: progressParams.total,
total: progressParams.total,
loaded: progressParams.loaded,
// deprecated https://codereview.chromium.org/492213004
position: progressParams.loaded,
lengthComputable: progressParams.lengthComputable,
bubbles: false,
cancelable: false
});
}