/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/
*
* class XMLHttpRequestEventTarget extends EventTarget
* class XMLHttpRequestUpload extends XMLHttpRequestEventTarget
* const XMLHttpRequestResponseType
* export class XMLHttpRequest extends XMLHttpRequestEventTarget
* export class ProgressEvent extends Event
* export class FormData
*
* function handleError
* function requestErrorSteps
* function setResponseToNetworkError
*
* function getResponseMimeType
* function getFinalMimeType
* function getFinalEncoding
* function setDocumentResponse
* function getTextResponse
*
* class ClientRequestData
* function requestDataURL
*
* @module whatwg-xhr
* @desc Main module - A Node.js implementation of the {@link https://xhr.spec.whatwg.org/ WHATWG XMLHttpRequest Living Standard} for non-browser environments.
* @version 1.0.0
* @author Essam A. El-Sherif
*/
/* Import nodeJS core modules */
import fs from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import process from 'node:process';
import threads from 'node:worker_threads';
import { spawn } from 'node:child_process';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Readable } from 'node:stream';
/**
* Global object available in all node modules >= v17.0.0
*
* DOMException Added in: node v17.0.0
*
* @external DOMException
* @desc Global object available in all node modules >= v17.0.0, defined by {@link https://webidl.spec.whatwg.org/#idl-DOMException WHATWG IDL Living Standard} and implemented by {@link https://nodejs.org/docs/latest/api/globals.html#domexception node.js}.
*/
/**
* Global object available in all node modules >= v15.4.0.
*
* @interface EventTarget
* @desc A browser-compatible implementation of the EventTarget class, defined by {@link https://dom.spec.whatwg.org/#eventtarget WHATWG DOM Living Standard} and implemented by {@link https://nodejs.org/docs/latest/api/globals.html#eventtarget node.js}.
*/
/**
* Global object available in all node modules >= v15.4.0.
*
* @interface Event
* @desc A browser-compatible implementation of the Event class, defined by {@link https://dom.spec.whatwg.org/#event WHATWG DOM Living Standard} and implemented by {@link https://nodejs.org/docs/latest/api/globals.html#event node.js}.
*/
/* Import npm-packages dependencies */
import whatwgEncoding from 'whatwg-encoding';
import htmlEncodingSniffer from 'html-encoding-sniffer';
/* Import from local helper modules */
import {
parse as contentTypeParse,
format as contentTypeFormat
} from './helper/content-type.js';
import {
dataURLProcessor,
parseMIMEType,
serializeAMimeType
} from './helper/whatwg-misc.js';
import {
forbiddenRequestHeader,
forbiddenHttpRequestMethod,
safelyExtractBodyWithType
} from './helper/whatwg-fetch.js';
import {
xmlEncodingSniffer
} from './helper/xml-encoding-sniffer.js';
/* Emulate commonJS __filename & __dirname global constants */
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* @const {boolean} disableHeaderCheck
* @static
* @desc Allow request-headers forbidden by {@link https://xhr.spec.whatwg.org/ XHR Specs}.
*/
export const disableHeaderCheck = false;
/**
* @func defaultHeaders
* @return {object} Default headers to include in each XMLHttpRequest.
*/
const defaultHeaders = () => ({
'User-Agent' : 'whatwg-xhr',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5'
});
/****/
/**
* @func defaultHeadersCase
* @return {object} Lower case default headers.
*/
const defaultHeadersCase = () => ({
'user-agent' : 'User-Agent',
'accept': 'Accept',
'accept-language': 'Accept-Language'
});
/****/
/** @const {WeakMap} xmlHttpRequestEventTarget - Map any XMLHttpRequestEventTarget object created to an associated data object. */
const xmlHttpRequestEventTarget = new WeakMap();
/** @const {WeakMap} xmlHttpRequest - Map any XMLHttpRequest object created to an associated data object. */
const xmlHttpRequest = new WeakMap();
/** @const {WeakMap} progressEvent - Map any ProgressEvent object created to an associated data object. */
const progressEvent = new WeakMap();
/** @const {WeakMap} formData - Map any FormData object created to an associated data object. */
const formData = new WeakMap();
let allowXMLHttpRequestUploadConstructor = false;
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#interface-xmlhttprequest
*
* interface XMLHttpRequestEventTarget : EventTarget{
* // event handlers
* attribute EventHandler onloadstart;
* attribute EventHandler onprogress;
* attribute EventHandler onabort;
* attribute EventHandler onerror;
* attribute EventHandler onload;
* attribute EventHandler ontimeout;
* attribute EventHandler onloadend;
* };
*
* @class XMLHttpRequestEventTarget
* @extends module:whatwg-xhr~EventTarget
* @desc Interface that describes the event handlers shared on XMLHttpRequest and XMLHttpRequestUpload interfaces.
*/
class XMLHttpRequestEventTarget extends EventTarget{
constructor(){
super();
const eventHandlers = {};
eventHandlers['onloadstart'] = null; // xhr.spec. attribute EventHandler
eventHandlers['onprogress' ] = null; // xhr.spec. attribute EventHandler
eventHandlers['onabort' ] = null; // xhr.spec. attribute EventHandler
eventHandlers['onerror' ] = null; // xhr.spec. attribute EventHandler
eventHandlers['onload' ] = null; // xhr.spec. attribute EventHandler
eventHandlers['ontimeout' ] = null; // xhr.spec. attribute EventHandler
eventHandlers['onloadend' ] = null; // xhr.spec. attribute EventHandler
xmlHttpRequestEventTarget.set(this, eventHandlers);
}
/**
* @member {function} onloadstart
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'loadstart' event is emitted.
*/
get onloadstart(){ return xmlHttpRequestEventTarget.get(this)['onloadstart']; }
set onloadstart(callback){
this.removeEventListener('loadstart', xmlHttpRequestEventTarget.get(this)['onloadstart']);
xmlHttpRequestEventTarget.get(this)['onloadstart'] = null;
if(typeof callback === 'function'){
this.addEventListener('loadstart', callback);
xmlHttpRequestEventTarget.get(this)['onloadstart'] = callback;
}
}
/**
* @member {function} onprogress
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'progress' event is emitted.
*/
get onprogress(){ return xmlHttpRequestEventTarget.get(this)['onprogress']; }
set onprogress(callback){
this.removeEventListener('progress', xmlHttpRequestEventTarget.get(this)['onprogress']);
xmlHttpRequestEventTarget.get(this)['onprogress'] = null;
if(typeof callback === 'function'){
this.addEventListener('progress', callback);
xmlHttpRequestEventTarget.get(this)['onprogress'] = callback;
}
}
/**
* @member {function} onabort
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'abort' event is emitted.
*/
get onabort(){ return xmlHttpRequestEventTarget.get(this)['onabort']; }
set onabort(callback){
this.removeEventListener('abort', xmlHttpRequestEventTarget.get(this)['onabort']);
xmlHttpRequestEventTarget.get(this)['onabort'] = null;
if(typeof callback === 'function'){
this.addEventListener('abort', callback);
xmlHttpRequestEventTarget.get(this)['onabort'] = callback;
}
}
/**
* @member {function} onerror
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'error' event is emitted.
*/
get onerror(){ return xmlHttpRequestEventTarget.get(this)['onerror']; }
set onerror(callback){
this.removeEventListener('error', xmlHttpRequestEventTarget.get(this)['onerror']);
xmlHttpRequestEventTarget.get(this)['onerror'] = null;
if(typeof callback === 'function'){
this.addEventListener('error', callback);
xmlHttpRequestEventTarget.get(this)['onerror'] = callback;
}
}
/**
* @member {function} onload
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'load' event is emitted.
*/
get onload(){ return xmlHttpRequestEventTarget.get(this)['onload']; }
set onload(callback){
this.removeEventListener('load', xmlHttpRequestEventTarget.get(this)['onload']);
xmlHttpRequestEventTarget.get(this)['onload'] = null;
if(typeof callback === 'function'){
this.addEventListener('load', callback);
xmlHttpRequestEventTarget.get(this)['onload'] = callback;
}
}
/**
* @member {function} ontimeout
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'timeout' event is emitted.
*/
get ontimeout(){ return xmlHttpRequestEventTarget.get(this)['ontimeout']; }
set ontimeout(callback){
this.removeEventListener('timeout', xmlHttpRequestEventTarget.get(this)['ontimeout']);
xmlHttpRequestEventTarget.get(this)['ontimeout'] = null;
if(typeof callback === 'function'){
this.addEventListener('timeout', callback);
xmlHttpRequestEventTarget.get(this)['ontimeout'] = callback;
}
}
/**
* @member {function} onloadend
* @memberof module:whatwg-xhr~XMLHttpRequestEventTarget
* @instance
* @desc Get/Set the event handler fired whenever the 'loadend' event is emitted.
*/
get onloadend(){ return xmlHttpRequestEventTarget.get(this)['onloadend']; }
set onloadend(callback){
this.removeEventListener('loadend', xmlHttpRequestEventTarget.get(this)['onloadend']);
xmlHttpRequestEventTarget.get(this)['onloadend'] = null;
if(typeof callback === 'function'){
this.addEventListener('loadend', callback);
xmlHttpRequestEventTarget.get(this)['onloadend'] = callback;
}
}
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#interface-xmlhttprequest
*
* interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {
* };
*
* @class XMLHttpRequestUpload
* @extends module:whatwg-xhr~XMLHttpRequestEventTarget
* @desc Interface that represents the upload process for a specific XMLHttpRequest.
*/
class XMLHttpRequestUpload extends XMLHttpRequestEventTarget{
constructor(){
super();
if(!allowXMLHttpRequestUploadConstructor)
throw new TypeError(`Illegal constructor.`);
}
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#interface-xmlhttprequest
*
* enum XMLHttpRequestResponseType{
* "",
* "arraybuffer",
* "blob",
* "document",
* "json",
* "text"
* };
*
* @const {object} XMLHttpRequestResponseType
* @desc Enumerated set of response types.
*/
const XMLHttpRequestResponseType = {
'': null,
'arraybuffer': null,
'blob': null,
'document': null,
'json': null,
'text': null,
};
Object.preventExtensions(XMLHttpRequestResponseType);
Object.freeze(XMLHttpRequestResponseType);
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#interface-xmlhttprequest
*
* interface XMLHttpRequest : XMLHttpRequestEventTarget{
* constructor();
*
* // event handler
* attribute EventHandler onreadystatechange;
*
* // states
* const unsigned short UNSENT = 0;
* const unsigned short OPENED = 1;
* const unsigned short HEADERS_RECEIVED = 2;
* const unsigned short LOADING = 3;
* const unsigned short DONE = 4;
*
* readonly attribute unsigned short readyState;
*
* // request
* undefined open(ByteString method, USVString url);
* undefined open(ByteString method, USVString url, boolean async,
* optional USVString? username = null, optional USVString? password = null);
* undefined setRequestHeader(ByteString name, ByteString value);
*
* attribute unsigned long timeout;
* attribute boolean withCredentials;
*
* [SameObject] readonly attribute XMLHttpRequestUpload upload;
*
* undefined send(optional (Document or XMLHttpRequestBodyInit)? body = null);
* undefined abort();
*
* // response
* readonly attribute USVString responseURL;
* readonly attribute unsigned short status;
* readonly attribute ByteString statusText;
*
* ByteString getResponseHeader(ByteString name);
* ByteString getAllResponseHeaders();
* undefined overrideMimeType(DOMString mime);
*
* attribute XMLHttpRequestResponseType responseType;
*
* readonly attribute any response;
* readonly attribute USVString responseText;
*
* [Exposed=Window] readonly attribute Document? responseXML;
* };
*
* @class XMLHttpRequest
* @extends module:whatwg-xhr~XMLHttpRequestEventTarget
* @static
* @desc Interface that represents the objects used to interact with a server.
*/
export class XMLHttpRequest extends XMLHttpRequestEventTarget{
constructor(){
super();
const requestData = {};
// event handler
requestData['onreadystatechange'] = null; // xhr.spec. attribute EventHandler
// states - An XMLHttpRequest object has an associated:
requestData['readyState'] = this.UNSENT; // xhr.spec. readonly attribute unsigned short state: initially unsent.
// requesAn XMLHttpRequest object has an associated:
requestData['timeout'] = 0; // xhr.spec. attribute unsigned long timeout: An unsigned integer, initially 0.
requestData['withCredentials'] = false; // xhr.spec. attribute boolean cross-origin credentials: A boolean, initially false.
allowXMLHttpRequestUploadConstructor = true; //
requestData['upload'] = new XMLHttpRequestUpload(); // xhr.spec. readonly attribute XMLHttpRequestUpload upload object: an XMLHttpRequestUpload object.
allowXMLHttpRequestUploadConstructor = false; //
// response - An XMLHttpRequest object has an associated:
requestData['status' ] = 0; // xhr.spec. readonly attribute unsigned short status;
requestData['statusText'] = ''; // xhr.spec. readonly attribute ByteString statusText;
requestData['response' ] = ''; // xhr.spec. readonly attribute any response: a response, initially a network error.
requestData['responseText'] = ''; // xhr.spec. readonly attribute ByteString responseText.
requestData['responseType'] = ''; // xhr.spec. response type: one of "", "arraybuffer", "blob", "document", "json", and "text"; initially "".
requestData['responseURL' ] = ''; // xhr.spec. readonly attribute USVString responseURL;
requestData['responseXML' ] = null; // xhr.spec. [Exposed=Window] readonly attribute Document? responseXML;
// associated data - An XMLHttpRequest object has an associated:
requestData['sendFlag'] = false; // xhr.spec. send() flag: a flag, initially unset.
requestData['method' ] = ''; // xhr.spec. request method: a method.
requestData['url' ] = null; // xhr.spec. request URL: A URL.
requestData['async' ] = true; // xhr.spec. synchronous flag: a flag, initially unset.
requestData['user' ] = null;
requestData['password'] = null;
requestData['requestHeaders' ] = {}; // xhr.spec. author request headers: a header list, initially empty.
requestData['requestHeadersCase'] = {};
requestData['requestBody' ] = null; // xhr.spec. request body: initially null.
requestData['uploadCompleteFlag'] = false; // xhr.spec. upload complete flag: a flag, initially unset.
requestData['uploadListenerFlag'] = false; // xhr.spec. upload listener flag: a flag, initially unset.
requestData['timeoutFlag' ] = false; // xhr.spec. timeout flag: a flag, initially unset.
requestData['receivedBytes' ] = Buffer.alloc(0); // xhr.spec. received bytes: a byte sequence, initially the empty byte sequence.
requestData['responseObject' ] = null; // xhr.spec. response object: an object, failure, or null, initially null.
requestData['fetchController' ] = null; // xhr.spec. fetch controller: a fetch controller, initially a new fetch controller.
requestData['overrideMimeType' ] = null; // xhr.spec. A MIME type or null, initially null. Can get a value when overrideMimeType() is invoked.
requestData['_redirectCount' ] = 0; // fetch.spec.2.2.5. A request has an associated redirect count. Unless stated otherwise, it is zero.
requestData['_responseAbort' ] = false; // fetch.spec.2.2.6. A response can have an associated aborted flag, which is initially unset.
requestData['_responseNetworkError'] = false;
requestData['_responseBody' ] = null; // fetch.spec.2.2.6. A response has an associated body (null or a body). Unless stated otherwise it is null.
xmlHttpRequest.set(this, requestData);
}
/**
* @member {function} onreadystatechange
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @desc Get/Set the event handler fired whenever there's change in readyState.
*/
get onreadystatechange(){ return xmlHttpRequest.get(this)['onreadystatechange']; }
set onreadystatechange(callback){
this.removeEventListener('readystatechange', xmlHttpRequest.get(this)['onreadystatechange']);
xmlHttpRequest.get(this)['onreadystatechange'] = null;
if(typeof callback === 'function'){
this.addEventListener('readystatechange', callback);
xmlHttpRequest.get(this)['onreadystatechange'] = callback;
}
}
/**
* @member {number} readyState
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get the state an XMLHttpRequest client is in.
*/
get readyState(){ return xmlHttpRequest.get(this)['readyState']; }
/**
* @member {object} upload
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get an XMLHttpRequestUpload object that can be observed to monitor an upload's progress.
*/
get upload(){ return xmlHttpRequest.get(this)['upload']; }
/**
* @member {number} status
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get the numerical HTTP status code of the server response.
*/
get status(){ return xmlHttpRequest.get(this)['status']; }
/**
* @member {string} statusText
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get the HTTP status message of the server response.
*/
get statusText(){ return xmlHttpRequest.get(this)['statusText']; }
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-response-attribute
*
* client.response
*
* Returns the response body.
*
* The response getter steps are:
* 1. If this’s response type is the empty string or "text", then:
* 1. If this’s state is not loading or done, then return the empty string.
* 2. Return the result of getting a text response for this.
* 2. If this’s state is not done, then return null.
* 3. If this’s response object is failure, then return null.
* 4. If this’s response object is non-null, then return it.
* 5. If this’s response type is "arraybuffer", then set this’s response object to a new ArrayBuffer object representing this’s received bytes.
* If this throws an exception, then set this’s response object to failure and return null.
* Note: Allocating an ArrayBuffer object is not guaranteed to succeed. [ECMASCRIPT]
* 6. Otherwise, if this’s response type is "blob", set this’s response object to a new Blob object representing this’s received bytes with type
* set to the result of get a final MIME type for this.
* 7. Otherwise, if this’s response type is "document", set a document response for this.
* 8. Otherwise:
* 1. Assert: this’s response type is "json".
* 2. If this’s response’s body is null, then return null.
* 3. Let jsonObject be the result of running parse JSON from bytes on this’s received bytes. If that threw an exception, then return null.
* 4. Set this’s response object to jsonObject.
* 9. Return this’s response object.
*
* @member {ArrayBuffer|Blob|Document|object|string} response
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get the response's body content received from a server following a request being sent.
*/
get response(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If this’s response type is the empty string or "text", then:
if(requestData['responseType'] === '' || requestData['responseType'] === 'text'){
// xhr.spec.1.1. If this’s state is not loading or done, then return the empty string.
if(requestData['readyState'] !== this.LOADING && requestData['readyState'] !== this.DONE){
return '';
}
// xhr.spec.1.2. Return the result of getting a text response for this.
return getTextResponse.call(this);
}
// xhr.spec.2. If this’s state is not done, then return null.
if(requestData['readyState'] !== this.DONE)
return null;
// xhr.spec.3. If this’s response object is failure, then return null.
if(requestData['responseObject'] === 'failure')
return null;
// xhr.spec.4. If this’s response object is non-null, then return it.
if(requestData['responseObject'] !== null)
return requestData['responseObject'];
// xhr.spec.5. If this’s response type is "arraybuffer"
if(requestData['responseType'] === 'arraybuffer'){
// xhr.spec.5. set this’s response object to a new ArrayBuffer object representing this’s received bytes.
try{
requestData['responseObject'] = new Uint8Array(requestData['receivedBytes']).buffer;
}
catch(e){
// xhr.spec.5. If this throws an exception, then set this’s response object to failure and return null.
requestData['responseObject'] = 'failure';
return null;
}
}
else
// xhr.spec.6. Otherwise, if this’s response type is "blob",
if(requestData['responseType'] === 'blob'){
// xhr.spec.6. set this’s response object to a new Blob object representing this’s received bytes with type
// set to the result of get a final MIME type for this.
if(!(requestData['responseObject'] instanceof Blob)){
requestData['responseObject'] = new Blob([requestData['receivedBytes']], {
endings: 'native',
type: getFinalMimeType.call(this)
});
}
}
else
// xhr.spec.7. Otherwise, if this’s response type is "document", set a document response for this.
if(requestData['responseType'] === 'document'){
// @todo...
}
// xhr.spec.8. Otherwise:
else{
// xhr.spec.8.1. Assert: this’s response type is "json".
if(requestData['responseType'] === 'json'){
// xhr.spec.8.2. If this’s response’s body is null, then return null.
// author. if(requestData['_responseBody'] === null)
if(requestData['receivedBytes'].length === 0)
return null;
// xhr.spec.8.3. Let jsonObject be the result of running parse JSON from bytes on this’s received bytes.
// If that threw an exception, then return null.
try{
let jsonObject = JSON.parse(requestData['receivedBytes']);
// xhr.spec.8.4. Set this’s response object to jsonObject.
requestData['responseObject'] = jsonObject;
}catch(e){
return null;
}
}
}
// xhr.spec.9. Return this’s response object.
return requestData['responseObject'];
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-responsetext-attribute
*
* client.responseText
*
* Returns response as text.
* Throws an "InvalidStateError" DOMException if responseType is not the empty string or "text".
*
* The responseText getter steps are:
* 1. If this’s response type is not the empty string or "text", then throw an "InvalidStateError" DOMException.
* 2. If this’s state is not loading or done, then return the empty string.
* 3. Return the result of <getting a text response> for this.
*
* @member {string} responseText
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get the text received from a server following a request being sent.
*/
get responseText(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If this’s response type is not the empty string or "text", then throw an "InvalidStateError" DOMException.
if(requestData['responseType'] !== '' && requestData['responseType'] !== 'text')
throw new DOMException(`XMLHttpRequest.responseText getter: responseText is only available if responseType is '' or 'text'.`, 'InvalidAccessError');
// xhr.spec.2. If this’s state is not loading or done, then return the empty string.
if(requestData['readyState'] !== this.LOADING && requestData['readyState'] !== this.DONE){
return '';
}
// xhr.spec.3. Return the result of <getting a text response> for this.
return getTextResponse.call(this);
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-responsetype-attribute
*
* client.responseType[ = value ]
*
* Returns the response type.
* Can be set to change the response type. Values are: the empty string (default), "arraybuffer", "blob", "document", "json", and "text".
* When set: setting to "document" is ignored if the current global object is not a Window object.
* When set: throws an "InvalidStateError" DOMException if state is loading or done.
* When set: throws an "InvalidAccessError" DOMException if the synchronous flag is set and the current global object is a Window object.
*
* The responseType getter steps are to return this’s response type.
* The responseType setter steps are:
* 1. If the current global object is not a Window object and the given value is "document", then return.
* 2. If this’s state is loading or done, then throw an "InvalidStateError" DOMException.
* 3. If the current global object is a Window object and this’s synchronous flag is set, then throw an "InvalidAccessError" DOMException.
* 4. Set this’s response type to the given value.
*
* @member {string} responseType
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @desc Get/Set an enumerated string value specifying the type of data contained in the response.
*/
get responseType(){
return xmlHttpRequest.get(this)['responseType'];
}
set responseType(type){
if(String(type) in XMLHttpRequestResponseType){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.3. If the current global object is a Window object and this’s synchronous flag is set,
// then throw an "InvalidAccessError" DOMException.
// @author. xhr.spec.3. will also be applicable to main thread
if(threads.isMainThread){
if(!requestData['async'])
throw new DOMException(
`XMLHttpRequest.responseType setter: synchronous XMLHttpRequests do not support timeout and responseType`,
'InvalidAccessError'
);
}
else{
// xhr.spec.1. If the current global object is not a Window object and the given value is "document", then return.
if(String(type).toLowerCase() === 'document')
return;
}
// xhr.spec.2. If this’s state is loading or done, then throw an "InvalidStateError" DOMException.
if(requestData['readyState'] === this.LOADING || requestData['readyState'] === this.DONE)
throw new DOMException(`XMLHttpRequest.responseType setter: Cannot set 'responseType' property on XMLHttpRequest after 'send()' (when its state is LOADING or DONE).`, 'InvalidStateError');
requestData['responseType'] = String(type).toLowerCase();
}
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-responsexml-attribute
*
* The responseXML getter
*
* client . responseXML
*
* Returns the response as document.
* Throws an "InvalidStateError" DOMException if responseType is not the empty string or "document".
*
* The responseXML getter steps are:
* 1. If this’s response type is not the empty string or "document", then throw an "InvalidStateError" DOMException.
* 2. If this’s state is not done, then return null.
* 3. Assert: this’s response object is not failure.
* 4. If this’s response object is non-null, then return it.
* 5. Set a document response for this.
* 6. Return this’s response object.
*
* @member {Document|null} responseXML
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get a Document containing the HTML or XML retrieved by the request, or null.
*/
get responseXML(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If this’s response type is not the empty string or "document", then throw an "InvalidStateError" DOMException.
if(this.responseType !== '' && this.responseType !== 'document'){
throw new DOMException(
`XMLHttpRequest.responseXML getter: responseXML is only available if responseType is '' or 'document'.`, 'InvalidStateError'); // firefox 102
}
// xhr.spec.2. If this’s state is not done, then return null.
if(xmlHttpRequest.get(this)['readyState'] !== this.DONE)
return null;
// xhr.spec.3. Assert: this’s response object is not failure.
if(requestData['responseObject'] === 'failure')
return null;
// xhr.spec.4. If this’s response object is non-null, then return it.
if(requestData['responseObject'] !== null)
return requestData['responseObject'];
// xhr.spec.5. Set a document response for this.
setDocumentResponse.call(this);
// xhr.spec.6. Return this’s response object.
return xmlHttpRequest.get(this)['responseObject'];
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-responseurl-attribute
*
* The responseURL getter
*
* The responseURL getter steps are to return the empty string if this’s response’s URL is null;
* otherwise its serialization with the exclude fragment flag set.
*
* @member {string} responseURL
* @memberof module:whatwg-xhr.XMLHttpRequest
* @readonly
* @instance
* @desc Get the serialized URL of the response or the empty string if the URL is null.
*/
get responseURL(){
const requestData = xmlHttpRequest.get(this);
if(requestData['response'] && requestData['response'].url){
return requestData['response'].url.replace(/#.*$/, '');
}
return '';
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-timeout-attribute
* client.timeout
*
* Can be set to a time in milliseconds. When set to a non-zero value will cause fetching to terminate after the given time has passed.
* When the time has passed, the request has not yet completed, and this’s synchronous flag is unset, a timeout event will then be dispatched,
* or a "TimeoutError" DOMException will be thrown otherwise (for the send() method).
* When set: throws an "InvalidAccessError" DOMException if the synchronous flag is set and the current global object is a Window object.
*
* The timeout getter steps are to return this’s timeout.
* The timeout setter steps are:
* 1. If the current global object is a Window object and this’s synchronous flag is set, then throw an "InvalidAccessError" DOMException.
* 2. Set this’s timeout to the given value.
* Note: This implies that the timeout attribute can be set while fetching is in progress. If that occurs it will still be measured relative
* to the start of fetching.
*
* @member {number} timeout
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @desc Get/Set an unsigned long representing the number of milliseconds a request can take before automatically being terminated.
*/
get timeout(){ return xmlHttpRequest.get(this)['timeout']; }
set timeout(int){
// xhr.spec.1. If the current global object is a Window object and this’s synchronous flag is set, then throw an "InvalidAccessError" DOMException.
if(globalThis.Window && !xmlHttpRequest.get(this)['async'])
throw new DOMException(`XMLHttpRequest.timeout setter: synchronous XMLHttpRequests do not support timeout and responseType`, 'InvalidAccessError');
// @author. xhr.spec.1. will also be applicable to main thread
if(threads.isMainThread && !xmlHttpRequest.get(this)['async'])
throw new DOMException(`XMLHttpRequest.timeout setter: synchronous XMLHttpRequests do not support timeout and responseType`, 'InvalidAccessError');
// xhr.spec.2. Set this’s timeout to the given value.
xmlHttpRequest.get(this)['timeout'] = isNaN(int) ? 0 : int;
}
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-withcredentials-attribute
* client.withCredentials
*
* True when credentials are to be included in a cross-origin request.
* False when they are to be excluded in a cross-origin request and when cookies are to be ignored in its response. Initially false.
* When set: throws an "InvalidStateError" DOMException if state is not unsent or opened, or if the send() flag is set.
*
* The withCredentials getter steps are to return this’s cross-origin credentials.
* The withCredentials setter steps are:
* 1. If this’s state is not unsent or opened, then throw an "InvalidStateError" DOMException.
* 2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
* 3. Set this’s cross-origin credentials to the given value.
*
* @member {boolean} withCredentials
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @desc Get/Set a boolean value that indicates whether or not cross-site Access-Control requests should be made using credentials.
*/
get withCredentials(){ return xmlHttpRequest.get(this)['withCredentials']; }
set withCredentials(cred = false){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If this’s state is not unsent or opened, then throw an "InvalidStateError" DOMException.
// xhr.spec.2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
if(requestData['readyState'] > this.OPENED || requestData['sendFlag'])
throw new DOMException(`XMLHttpRequest.withCredentials setter: XMLHttpRequest must not be sending.`, 'InvalidStateError');
// xhr.spec.3. Set this’s cross-origin credentials to the given value.
xmlHttpRequest.get(this)['withCredentials'] = Boolean(cred);
}
} // @endof class XMLHttpRequest extends XMLHttpRequestEventTarget
/**
* @const {number} UNSENT
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @default 0
* @desc State of XMLHttpRequest object, client has been created, open() not called yet.
*/
Object.defineProperty(XMLHttpRequest.prototype, 'UNSENT', {value: 0, writable: false, enumerable: true, configurable: false});
/**
* @const {number} OPENED
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @default 1
* @desc State of XMLHttpRequest object, open() has been called.
*/
Object.defineProperty(XMLHttpRequest.prototype, 'OPENED', {value: 1, writable: false, enumerable: true, configurable: false});
/**
* @const {number} HEADERS_RECEIVED
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @default 2
* @desc State of XMLHttpRequest object, send() has been called, headers and status are available.
*/
Object.defineProperty(XMLHttpRequest.prototype, 'HEADERS_RECEIVED', {value: 2, writable: false, enumerable: true, configurable: false});
/**
* @const {number} LOADING
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @default 3
* @desc State of XMLHttpRequest object, downloading, responseText holds partial data.
*/
Object.defineProperty(XMLHttpRequest.prototype, 'LOADING', {value: 3, writable: false, enumerable: true, configurable: false});
/**
* @const {number} DONE
* @memberof module:whatwg-xhr.XMLHttpRequest
* @instance
* @default 4
* @desc State of XMLHttpRequest object, the operation is complete.
*/
Object.defineProperty(XMLHttpRequest.prototype, 'DONE', {value: 4, writable: false, enumerable: true, configurable: false});
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-open()-method
*
* client.open(method, url [, async = true [, username = null [, password = null]]])
*
* Sets the request method, request URL, and synchronous flag.
* Throws a "SyntaxError" DOMException if either method is not a valid method or url cannot be parsed.
* Throws a "SecurityError" DOMException if method is a case-insensitive match for `CONNECT`, `TRACE`, or `TRACK`.
* Throws an "InvalidAccessError" DOMException if async is false, the current global object is a Window object, and the timeout attribute is not zero
* or the responseType attribute is not the empty string.
*
* Synchronous XMLHttpRequest outside of workers is in the process of being removed from the web platform as it has detrimental effects to the end user’s
* experience. (This is a long process that takes many years.) Developers must not pass false for the async argument when the current global object is a
* Window object. User agents are strongly encouraged to warn about such usage in developer tools and may experiment with throwing an "InvalidAccessError"
* DOMException when it occurs.
*
* The open(method, url) and open(method, url, async, username, password) method steps are:
*
* 1. If this’s relevant global object is a Window object and its associated Document is not fully active, then throw an "InvalidStateError" DOMException.
* 2. If method is not a method, then throw a "SyntaxError" DOMException.
* 3. If method is a forbidden method, then throw a "SecurityError" DOMException.
* 4. Normalize method.
* 5. Let parsedURL be the result of parsing url with this’s relevant settings object’s API base URL and this’s relevant settings object’s API URL
* character encoding.
* 6. If parsedURL is failure, then throw a "SyntaxError" DOMException.
* 7. If the async argument is omitted, set async to true, and set username and password to null.
* Note: Unfortunately legacy content prevents treating the async argument being undefined identical from it being omitted.
* 8. If parsedURL’s host is non-null, then:
* 1. If the username argument is not null, set the username given parsedURL and username.
* 2. If the password argument is not null, set the password given parsedURL and password.
* 9. If async is false, the current global object is a Window object, and either this’s timeout is not 0 or this’s response type is not the empty
* string, then throw an "InvalidAccessError" DOMException.
* 10. Terminate this’s fetch controller.
* Note: A fetch can be ongoing at this point.
* 11. Set variables associated with the object as follows:
* Unset this’s send() flag.
* Unset this’s upload listener flag.
* Set this’s request method to method.
* Set this’s request URL to parsedURL.
* Set this’s synchronous flag if async is false; otherwise unset this’s synchronous flag.
* Empty this’s author request headers.
* Set this’s response to a network error.
* Set this’s received bytes to the empty byte sequence.
* Set this’s response object to null.
* Note: Override MIME type is not overridden here as the overrideMimeType() method can be invoked before the open() method.
* 12. If this’s state is not opened, then:
* 1. Set this’s state to opened.
* 2. Fire an event named readystatechange at this.
*
* @method open
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @param {string} method - The HTTP request method to use, such as "GET", "POST", "PUT", "DELETE", etc. Ignored for non-HTTP(S) URLs.
* @param {string} url - A string or any other object with a stringifier that provides the URL of the resource to request.
* @param {boolean} is_async - (Optional) Boolean parameter, defaulting to true, indicating whether or not to perform the operation asynchronously.
* @param {string} username - (Optional) user name to use for authentication purposes; by default, this is the null value.
* @param {string} password - (Optional) password to use for authentication purposes; by default, this is the null value.
* @desc XMLHttpRequest method that initializes a newly-created request, or re-initializes an existing one.
* @requires module:whatwg-fetch.forbiddenHttpRequestMethod
*/
XMLHttpRequest.prototype.open = function open(method, url, is_async = true, username = null, password = null){
// xhr.spec.1. If this’s relevant global object is a Window object and its associated Document is not fully active, then throw an "InvalidStateError" DOMException.
if(globalThis.Window && !document.isConnected)
throw new DOMException(`XMLHttpRequest.open: ......`, 'InvalidStateError');
// xhr.spec.2. If method is not a method, then throw a "SyntaxError" DOMException
if(typeof method === 'function')
throw new DOMException(`XMLHttpRequest.open: an invalid or illegal method string was specified`, 'SyntaxError');
// xhr.spec.2. If method is not a method, then throw a "SyntaxError" DOMException
method = String(method);
if(/[^\w]/.test(method))
throw new DOMException(`XMLHttpRequest.open: an invalid or illegal method string was specified`, 'SyntaxError');
// xhr.spec.3. If method is a forbidden method, then throw a "SecurityError" DOMException
if(forbiddenHttpRequestMethod.has(String(method).toLowerCase()))
throw new DOMException(`XMLHttpRequest.open: the operation is insecure.`, 'SecurityError');
// xhr.spec.4. Normalize method.
// fetch.spec. To normalize a method, if it is a byte-case-insensitive match for `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`, or `PUT`, byte-uppercase it.
method = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'].find(m => m === String(method).toUpperCase()) || String(method);
// xhr.spec.5. Let parsedURL be the result of parsing url with this’s relevant settings object’s API base URL and
// this’s relevant settings object’s API URL character encoding.
let parsedURL;
try{
parsedURL = new URL(url);
}
catch(e){
// xhr.spec.6. If parsedURL is failure, then throw a "SyntaxError" DOMException
throw new DOMException(`XMLHttpRequest.open: an invalid or illegal url string was specified`, 'SyntaxError');
}
// xhr.spec.7. If the async argument is omitted, set async to true, and set username and password to null.
// @author: already handled by default arguments: open(method, url, is_async = true, username = null, password = null)
// xhr.spec.8. If parsedURL’s host is non-null, then:
if(parsedURL.host){
// xhr.spec.8.1. If the username argument is not null, set the username given parsedURL and username.
if(username){
/*
* URL Living Standard — Last Updated 24 May 2023
* url: https://url.spec.whatwg.org/#set-the-username
*
* To set the username given a url and username, set url’s username to the result of running UTF-8 percent-encode on username
* using the userinfo percent-encode set.
*/
parsedURL.username = username;
}
// xhr.spec.8.2. If the password argument is not null, set the password given parsedURL and password.
if(password){
/*
* URL Living Standard — Last Updated 24 May 2023
* url: https://url.spec.whatwg.org/#set-the-password
*
* To set the password given a url and password, set url’s password to the result of running UTF-8 percent-encode on password
* using the userinfo percent-encode set.
*/
parsedURL.password = password;
}
}
const requestData = xmlHttpRequest.get(this);
// xhr.spec.9. If async is false, the current global object is a Window object, and either this’s timeout is not 0 or this’s response type is not the empty
// string, then throw an "InvalidAccessError" DOMException.
if(globalThis.Window && is_async === false && (requestData['timeout'] || requestData['responseType']))
throw new DOMException(`XMLHttpRequest.open: synchronous XMLHttpRequests do not support timeout and responseType`, 'InvalidAccessError');
// @author. xhr.spec.9. will also be applicable to main thread
if(threads.isMainThread && is_async === false && (requestData['timeout'] || requestData['responseType']))
throw new DOMException(`XMLHttpRequest.open: synchronous XMLHttpRequests do not support timeout and responseType`, 'InvalidAccessError');
// xhr.spec.10. Terminate this’s fetch controller. Note: A fetch can be ongoing at this point.
if(requestData['fetchController']){
requestData['fetchController'].destroy();
requestData['fetchController'] = null;
}
// xhr.spec.11. Set variables associated with the object as follows:
// xhr.spec.11. Unset this’s send() flag.
requestData['sendFlag'] = false;
// xhr.spec.11. Unset this’s upload listener flag.
requestData['uploadListenerFlag'] = false;
// xhr.spec.11. Set this’s request method to method.
requestData['method'] = method;
// xhr.spec.11. Set this’s request URL to parsedURL.
requestData['url'] = parsedURL.href;
// xhr.spec.11. Set this’s synchronous flag if async is false; otherwise unset this’s synchronous flag.
requestData['async'] = !(is_async === false);
// xhr.spec.11. Empty this’s author request headers.
requestData['requestHeaders' ] = {};
requestData['requestHeadersCase'] = {};
// xhr.spec.11. Set this’s response to a network error.
setResponseToNetworkError.call(this);
// xhr.spec.11. Set this’s received bytes to the empty byte sequence.
requestData['receivedBytes'] = Buffer.alloc(0);
// xhr.spec.11. Set this’s response object to null.
requestData['responseObject'] = null;
// xhr.spec.12. If this’s state is not opened, then:
if(requestData['readyState'] !== this.OPENED){
// xhr.spec.12.1. Set this’s state to opened.
requestData['readyState'] = this.OPENED;
// xhr.spec.12.2. Fire an event named readystatechange at this.
this.dispatchEvent(new Event('readystatechange'));
}
}; // @endof method XMLHttpRequest.prototype.open
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-abort()-method
*
* client.abort()
* Cancels any network activity.
*
* The abort() method steps are:
* 1. Abort this’s fetch controller.
* 2. If this’s state is opened with this’s send() flag set, headers received, or loading, then run the request error steps for this and abort.
* 3. If this’s state is done, then set this’s state to unsent and this’s response to a network error.
* Note: No readystatechange event is dispatched.
*
* @method abort
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @desc XMLHttpRequest method that aborts the request if it has already been sent.
*/
XMLHttpRequest.prototype.abort = function abort(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. Abort this’s fetch controller
if(requestData['fetchController']){
requestData['fetchController'].destroy();
requestData['fetchController'] = null;
}
// xhr.spec.2. If this’s state is opened with this’s send() flag set, headers received,
// or loading, then run the request error steps for this and abort.
if(
requestData['readyState'] === this.OPENED && requestData['sendFlag'] ||
requestData['readyState'] === this.HEADERS_RECEIVED ||
requestData['readyState'] === this.LOADING
){
requestErrorSteps.call(this, 'abort');
}
// xhr.spec.3. If this’s state is done, then set this’s state to unsent and this’s response to a network error.
if(requestData['readyState'] === this.DONE){
requestData['readyState'] = this.UNSENT;
setResponseToNetworkError.call(this);
}
}; // @endof method XMLHttpRequest.prototype.abort
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-setrequestheader()-method
*
* client.setRequestHeader(name, value)
* Appends a value to an existing request header or adds a new request header.
* Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set.
* Throws a "SyntaxError" DOMException if name is not a header name or if value is not a header value.
*
* The setRequestHeader(name, value) method must run these steps:
* 1. If this’s state is not opened, then throw an "InvalidStateError" DOMException.
* 2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
* 3. Normalize value.
* 4. If name is not a header name or value is not a header value, then throw a "SyntaxError" DOMException.
* 5. An empty byte sequence represents an empty header value.
* 6. If (name, value) is a forbidden request-header, then return.
* 7. Combine (name, value) in this’s author request headers.
*
* @method setRequestHeader
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @param {string} name - The name of the header whose value is to be set.
* @param {string} value - The value to set as the body of the header.
* @desc XMLHttpRequest method that sets the value of an HTTP request header.
* @requires module:whatwg-fetch.forbiddenRequestHeader
*/
XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(name, value){
// @author: validate the number of arguments
if(arguments.length < 2)
throw new TypeError(`XMLHttpRequest.setRequestHeader: At least 2 arguments required, but only ${arguments.length} passed`); // firefox 102
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If this’s state is not opened, then throw an "InvalidStateError" DOMException.
if(requestData['readyState'] !== this.OPENED)
throw new DOMException(`XMLHttpRequest.send: XMLHttpRequest state must be OPENED.`, 'InvalidStateError'); // firefox 102
// xhr.spec.2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
if(requestData['sendFlag'])
throw new DOMException(`XMLHttpRequest.send: XMLHttpRequest must not be sending.`, 'InvalidStateError'); // firefox 102
// xhr.spec.3. Normalize value.
// fetch.spec. To normalize a byte sequence potentialValue, remove any leading and trailing HTTP whitespace bytes from potentialValue.
value = String(value).trim();
// xhr.spec.4. If name is not a header name or value is not a header value, then throw a "SyntaxError" DOMException.
try{
http.validateHeaderName(name);
}
catch(err){
name = String(name);
for(let i = 0; i < name.length; i++){
if(name[i].codePointAt(0) > 0xFF){
let msg = '';
msg += `XMLHttpRequest.setRequestHeader: `;
msg += `Cannot convert argument 1 to ByteString because the character at index ${i} `;
msg += `has value ${name[i].codePointAt(0)} which is greater than 255.`
throw new TypeError(msg); // firefox 102
}
}
throw new DOMException(`XMLHttpRequest.setRequestHeader: Invalid header name.`, 'SyntaxError'); // firefox 102
}
try{
http.validateHeaderValue(name, value);
}
catch(err){
for(let i = 0; i < value.length; i++){
if(value[i].codePointAt(0) > 0xFF){
let msg = '';
msg += `XMLHttpRequest.setRequestHeader: `;
msg += `Cannot convert argument 2 to ByteString because the character at index ${i} `;
msg += `has value ${value[i].codePointAt(0)} which is greater than 255.`
throw new TypeError(msg); // firefox 102
}
}
throw new DOMException(`XMLHttpRequest.setRequestHeader: Invalid header value.`, 'SyntaxError'); // firefox 102
}
// xhr.spec.6. If (name, value) is a forbidden request-header, then return.
if(forbiddenRequestHeader(name, value)){
return;
}
const headers = requestData['requestHeaders'];
const headersCase = requestData['requestHeadersCase'];
// xhr.spec.7. Combine (name, value) in this’s author request headers.
if(headersCase[name.toLowerCase()]){
let nameCase = headersCase[name.toLowerCase()];
headers[nameCase] = headers[nameCase] ? headers[nameCase] + ', ' + value : value;
}
else{
headersCase[name.toLowerCase()] = name;
headers[name] = value;
}
}; // @endof method XMLHttpRequest.prototype.setRequestHeader
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-send()-method
*
* client.send([body = null])
* Initiates the request. The body argument provides the request body, if any, and is ignored if the request method is GET or HEAD.
* Throws an "InvalidStateError" DOMException if either state is not opened or the send() flag is set.
*
* The send(body) method steps are:
* 1. If this’s state is not opened, then throw an "InvalidStateError" DOMException.
* 2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
* 3. If this’s request method is `GET` or `HEAD`, then set body to null.
* 4. If body is not null, then:
* 1. Let extractedContentType be null.
* 2. If body is a Document, then set this’s request body to body, serialized, converted, and UTF-8 encoded.
* 3. Otherwise:
* 1. Let bodyWithType be the result of safely extracting body.
* 2. Set this’s request body to bodyWithType’s body.
* 3. Set extractedContentType to bodyWithType’s type.
* 4. Let originalAuthorContentType be the result of getting `Content-Type` from this’s author request headers.
* 5. If originalAuthorContentType is non-null, then:
* 1. If body is a Document or a USVString, then:
* 1. Let contentTypeRecord be the result of parsing originalAuthorContentType.
* 2. If contentTypeRecord is not failure, contentTypeRecord’s parameters["charset"] exists, and parameters["charset"] is not an ASCII case-insensitive
* match for "UTF-8", then:
* 1. Set contentTypeRecord’s parameters["charset"] to "UTF-8".
* 2. Let newContentTypeSerialized be the result of serializing contentTypeRecord.
* 3. Set (`Content-Type`, newContentTypeSerialized) in this’s author request headers.
* 6. Otherwise:
* 1. If body is an HTML document, then set (`Content-Type`, `text/html;charset=UTF-8`) in this’s author request headers.
* 2. Otherwise, if body is an XML document, set (`Content-Type`, `application/xml;charset=UTF-8`) in this’s author request headers.
* 3. Otherwise, if extractedContentType is not null, set (`Content-Type`, extractedContentType) in this’s author request headers.
* 5. If one or more event listeners are registered on this’s upload object, then set this’s upload listener flag.
* 6. Let req be a new request, initialized as follows:
* method This’s request method.
* URL This’s request URL.
* header list This’s author request headers.
* unsafe-request flag Set.
* body This’s request body.
* client This’s relevant settings object.
* mode "cors".
* use-CORS-preflight flag Set if this’s upload listener flag is set.
* credentials mode If this’s cross-origin credentials is true, then "include"; otherwise "same-origin".
* use-URL-credentials flag Set if this’s request URL includes credentials.
* initiator type "xmlhttprequest".
* 7. Unset this’s upload complete flag.
* 8. Unset this’s timed out flag.
* 9. If req’s body is null, then set this’s upload complete flag.
* 10. Set this’s send() flag.
* 11. If this’s synchronous flag is unset, then:
* 1. Fire a progress event named loadstart at this with 0 and 0.
* 2. Let requestBodyTransmitted be 0.
* 3. Let requestBodyLength be req’s body’s length, if req’s body is non-null; otherwise 0.
* 4. Assert: requestBodyLength is an integer.
* 5. If this’s upload complete flag is unset and this’s upload listener flag is set, then fire a progress event named loadstart at this’s upload object
* with requestBodyTransmitted and requestBodyLength.
* 6. If this’s state is not opened or this’s send() flag is unset, then return.
* 7. Let processRequestBodyChunkLength, given a bytesLength, be these steps:
* 1. Increase requestBodyTransmitted by bytesLength.
* 2. If not roughly 50ms have passed since these steps were last invoked, then return.
* 3. If this’s upload listener flag is set, then fire a progress event named progress at this’s upload object with requestBodyTransmitted and
* requestBodyLength.
* Note: These steps are only invoked when new bytes are transmitted.
* 8. Let processRequestEndOfBody be these steps:
* 1. Set this’s upload complete flag.
* 2. If this’s upload listener flag is unset, then return.
* 3. Fire a progress event named progress at this’s upload object with requestBodyTransmitted and requestBodyLength.
* 4. Fire a progress event named load at this’s upload object with requestBodyTransmitted and requestBodyLength.
* 5. Fire a progress event named loadend at this’s upload object with requestBodyTransmitted and requestBodyLength.
* 9. Let processResponse, given a response, be these steps:
* 1. Set this’s response to response.
* 2. Handle errors for this.
* 3. If this’s response is a network error, then return.
* 4. Set this’s state to headers received.
* 5. Fire an event named readystatechange at this.
* 6. If this’s state is not headers received, then return.
* 7. If this’s response’s body is null, then run handle response end-of-body for this and return.
* 8. Let length be the result of extracting a length from this’s response’s header list.
* 9. If length is not an integer, then set it to 0.
* 10. Let processBodyChunk given bytes be these steps:
* 1. Append bytes to this’s received bytes.
* 2. If not roughly 50ms have passed since these steps were last invoked, then return.
* 3. If this’s state is headers received, then set this’s state to loading.
* 4. Fire an event named readystatechange at this.
* Note: Web compatibility is the reason readystatechange fires more often than this’s state changes.
* 5. Fire a progress event named progress at this with this’s received bytes’s length and length.
* 11. Let processEndOfBody be this step: run handle response end-of-body for this.
* 12. Let processBodyError be these steps:
* 1. Set this’s response to a network error.
* 2. Run handle errors for this.
* 13. Incrementally read this’s response’s body, given processBodyChunk, processEndOfBody, processBodyError, and this’s relevant global object.
* 10. Set this’s fetch controller to the result of fetching req with processRequestBodyChunkLength set to processRequestBodyChunkLength,
* processRequestEndOfBody set to processRequestEndOfBody, and processResponse set to processResponse.
* 11. Let now be the present time.
* 12. Run these steps in parallel:
* 1. Wait until either req’s done flag is set or this’s timeout is not 0 and this’s timeout milliseconds have passed since now.
* 2. If req’s done flag is unset, then set this’s timed out flag and terminate this’s fetch controller.
* 12. Otherwise, if this’s synchronous flag is set:
* 1. Let processedResponse be false.
* 2. Let processResponseConsumeBody, given a response and nullOrFailureOrBytes, be these steps:
* 1. If nullOrFailureOrBytes is not failure, then set this’s response to response.
* 2. If nullOrFailureOrBytes is a byte sequence, then append nullOrFailureOrBytes to this’s received bytes.
* 3. Set processedResponse to true.
* 3. Set this’s fetch controller to the result of fetching req with processResponseConsumeBody set to processResponseConsumeBody and
* useParallelQueue set to true.
* 4. Let now be the present time.
* 5. Pause until either processedResponse is true or this’s timeout is not 0 and this’s timeout milliseconds have passed since now.
* 6. If processedResponse is false, then set this’s timed out flag and terminate this’s fetch controller.
* 7. Report timing for this’s fetch controller given the current global object.
* 8. Run handle response end-of-body for this.
*
* To handle response end-of-body for an XMLHttpRequest object xhr, run these steps:
* 1. Handle errors for xhr.
* 2. If xhr’s response is a network error, then return.
* 3. Let transmitted be xhr’s received bytes’s length.
* 4. Let length be the result of extracting a length from this’s response’s header list.
* 5. If length is not an integer, then set it to 0.
* 6. If xhr’s synchronous flag is unset, then fire a progress event named progress at xhr with transmitted and length.
* 7. Set xhr’s state to done.
* 8. Unset xhr’s send() flag.
* 9. Fire an event named readystatechange at xhr.
* 10. Fire a progress event named load at xhr with transmitted and length.
* 11. Fire a progress event named loadend at xhr with transmitted and length.
*
* @method send
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @param {Document| Blob|ArrayBuffer|TypedArray|DataView|FormData|URLSearchParams|string|null} body - The request body.
* @desc XMLHttpRequest method that sends the request to the server.
* @requires module:whatwg-fetch.safelyExtractBodyWithType
* @requires module:helper-content-type.parse
* @requires module:helper-content-type.format
*/
XMLHttpRequest.prototype.send = function send(body = null){
const xhr = this;
const requestData = xmlHttpRequest.get(xhr);
// xhr.spec.1. If this’s state is not opened, then throw an "InvalidStateError" DOMException.
if(requestData['readyState'] !== xhr.OPENED)
throw new DOMException(`XMLHttpRequest.send: XMLHttpRequest state must be OPENED.`, 'InvalidStateError'); // firefox 102
// xhr.spec.2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException.
if(requestData['sendFlag'])
throw new DOMException(`XMLHttpRequest.send: XMLHttpRequest must not be sending.`, 'InvalidStateError'); // firefox 102
// xhr.spec.3. If this’s request method is `GET` or `HEAD`, then set body to null.
if(requestData['method'].toUpperCase() === 'GET' || requestData['method'].toUpperCase() === 'HEAD'){
body = null;
}
// xhr.spec.4. If body is not null, then:
if(body !== null && body !== ''){
// xhr.spec.4.1. Let extractedContentType be null.
let extractedContentType = null;
// xhr.spec.4.2. If body is a Document, then set this’s request body to body, serialized, converted, and UTF-8 encoded.
if(globalThis.Document && body instanceof globalThis.Document){
// @todo: xhr.spec.4.2. set this’s request body to body, serialized, converted, and UTF-8 encoded.
}
// xhr.spec.4.3. Otherwise:
else{
// xhr.spec.4.3.1. Let bodyWithType be the result of safely extracting body.
let bodyWithType = safelyExtractBodyWithType(body);
// xhr.spec.4.3.2. Set this’s request body to bodyWithType’s body.
requestData['requestBody'] = bodyWithType.body;
// xhr.spec.4.3.3. Set extractedContentType to bodyWithType’s type.
extractedContentType = bodyWithType.type;
}
// xhr.spec.4.4. Let originalAuthorContentType be the result of getting `Content-Type` from this’s author request headers.
let originalAuthorContentType = null;
if(requestData['requestHeadersCase']['content-type'])
originalAuthorContentType = requestData['requestHeaders'][requestData['requestHeadersCase']['content-type']];
// xhr.spec.4.5. If originalAuthorContentType is non-null, then:
if(originalAuthorContentType !== null){
// xhr.spec.4.5.1. If body is a Document or a USVString, then:
if(
(globalThis.Document && body instanceof globalThis.Document) ||
typeof body === 'string' || typeof requestData['requestBody'].source === 'string'){
try{
// xhr.spec.4.5.1.1. Let contentTypeRecord be the result of parsing originalAuthorContentType.
const contentTypeRecord = contentTypeParse(originalAuthorContentType);
// xhr.spec.4.5.1.2. If contentTypeRecord is not failure,
// contentTypeRecord’s parameters["charset"] exists,
// and parameters["charset"] is not an ASCII case-insensitive match for "UTF-8", then:
let charset = contentTypeRecord.parameters['charset'];
if(charset && charset.toUpperCase() !== 'UTF-8'){
// xhr.spec.4.5.1.2.1. Set contentTypeRecord’s parameters["charset"] to "UTF-8".
contentTypeRecord.parameters['charset'] = 'UTF-8';
// xhr.spec.4.5.1.2.2. Let newContentTypeSerialized be the result of serializing contentTypeRecord.
let newContentTypeSerialized = contentTypeFormat(contentTypeRecord);
// xhr.spec.4.5.1.2.3. Set (`Content-Type`, newContentTypeSerialized) in this’s author request headers.
// @author: first delete the existing Content-Type header
delete requestData['requestHeaders'][requestData['requestHeadersCase']['content-type']];
delete requestData['requestHeadersCase']['content-type'];
this.setRequestHeader('Content-Type', newContentTypeSerialized);
}
}catch(e){}
}
}
// xhr.spec.4.6. Otherwise:
else{
// xhr.spec.4.6.1. If body is an HTML document, then set (`Content-Type`, `text/html;charset=UTF-8`) in this’s author request headers.
if(globalThis.Document && body instanceof globalThis.Document && body.type === 'html')
this.setRequestHeader('Content-Type', 'text/html;charset=UTF-8');
else
// xhr.spec.4.6.2. Otherwise, if body is an XML document, set (`Content-Type`, `application/xml;charset=UTF-8`) in this’s author request headers.
if(globalThis.Document && body instanceof globalThis.Document && body.type === 'xml')
this.setRequestHeader('Content-Type', 'application/xml;charset=UTF-8');
else
// xhr.spec.4.6.3. Otherwise, if extractedContentType is not null, set (`Content-Type`, extractedContentType) in this’s author request headers.
if(extractedContentType){
this.setRequestHeader('Content-Type', extractedContentType);
}
}
} // @endof if(body !== null && body !== undefined)
// xhr.spec.5. If one or more event listeners are registered on this’s upload object, then set this’s upload listener flag.
if(
requestData['upload']['onloadstart'] !== null ||
requestData['upload']['onprogress' ] !== null ||
requestData['upload']['onabort' ] !== null ||
requestData['upload']['onerror' ] !== null ||
requestData['upload']['onload' ] !== null ||
requestData['upload']['ontimeout' ] !== null ||
requestData['upload']['onloadend' ] !== null
){
requestData['uploadListenerFlag'] = true;
}
else
if(Object.getOwnPropertySymbols(requestData['upload']).filter(e => requestData['upload'][e].size).length){
requestData['uploadListenerFlag'] = true;
}
// xhr.spec.6. Let req be a new request, initialized as follows:
let req_auth = requestData['user'] ? `${requestData['user']}:${requestData['password'] === undefined ? '' : requestData['password']}` : undefined;
// xhr.spec.6. method: This’s request method.
let req_method = requestData['method'];
// xhr.spec.6. URL: This’s request URL.
let req_url = new URL(requestData['url']);
let req_host = req_url.hostname;
let req_port = req_url.port || (req_url.protocol === 'https:' ? 443 : 80);
let req_url_path = req_url.pathname + (req_url.search ? req_url.search : '');
let req_protocol = req_url.protocol;
// xhr.spec.6. header list: This’s author request headers.
const req_headers = requestData['requestHeaders'];
const req_headersCase = requestData['requestHeadersCase'];
for(let name in defaultHeaders()){
if(!req_headersCase[name.toLowerCase()]){
xhr.setRequestHeader(name, defaultHeaders()[name]);
}
}
// xhr.spec.6. unsafe-request flag: Set.
// xhr.spec.6. body: This’s request body.
// xhr.spec.6. client: This’s relevant settings object.
// xhr.spec.6. mode: "cors".
// xhr.spec.6. use-CORS-preflight flag: Set if this’s upload listener flag is set.
// xhr.spec.6. credentials mode: If this’s cross-origin credentials is true, then "include"; otherwise "same-origin".
// xhr.spec.6. use-URL-credentials flag: Set if this’s request URL includes credentials.
// xhr.spec.6. initiator type: "xmlhttprequest".
// author. use http.request(url[, options][, callback])
const clientRequestOptions = {
agent: false,
auth: req_auth,
headers: req_headers,
host: req_host,
port: req_port,
method: req_method,
path: req_url_path,
protocol: req_url.protocol,
setHost: true,
timeout: requestData['timeout'],
withCredentials: requestData['withCredentials'],
// insecureHTTPParser: true,
url: requestData['url'],
};
// author. choose the proper protocol
let doClientRequest;
if(req_protocol === 'https:')
doClientRequest = https.request;
else
if(req_protocol === 'http:')
doClientRequest = http.request;
else
if(req_protocol === 'data:')
doClientRequest = requestDataURL;
else
doClientRequest = http.request;
// xhr.spec.7. Unset this’s upload complete flag.
requestData['uploadCompleteFlag'] = false;
// xhr.spec.8. Unset this’s timed out flag.
requestData['timeoutFlag'] = false;
// xhr.spec.9. If req’s body is null, then set this’s upload complete flag.
if(requestData['requestBody'] === null)
requestData['uploadCompleteFlag'] = true;
// xhr.spec.10. Set this’s send() flag.
requestData['sendFlag'] = true;
// xhr.spec.11. If this’s synchronous flag is unset, then:
if(requestData['async']){
// xhr.spec.11.1. Fire a progress event named loadstart at this with 0 and 0.
xhr.dispatchEvent(new ProgressEvent('loadstart', {loaded: 0, total: 0}));
// xhr.spec.11.2. Let requestBodyTransmitted be 0.
let requestBodyTransmitted = 0;
// xhr.spec.11.3. Let requestBodyLength be req’s body’s length, if req’s body is non-null; otherwise 0.
let requestBodyLength = requestData['requestBody'] ? requestData['requestBody'].length : 0;
// xhr.spec.11.4. Assert: requestBodyLength is an integer.
if(isNaN(requestBodyLength))
requestBodyLength = 0;
// xhr.spec.11.5. If this’s upload complete flag is unset and this’s upload listener flag is set,
// then fire a progress event named loadstart at this’s upload object with requestBodyTransmitted and requestBodyLength.
if(!requestData['uploadCompleteFlag'] && requestData['uploadListenerFlag'])
requestData['upload'].dispatchEvent(new ProgressEvent('loadstart', {loaded: requestBodyTransmitted, total: requestBodyLength}));
// xhr.spec.11.6. If this’s state is not opened or this’s send() flag is unset, then return.
if(requestData['readyState'] !== xhr.OPENED || !requestData['sendFlag'])
return;
// xhr.spec.11.7. Let processRequestBodyChunkLength, given a bytesLength, be these steps:
// Note: These steps are only invoked when new bytes are transmitted.
const processRequestBodyChunkLength = () => {
let bytesLength = requestData['requestBody'].length; // @?????
// xhr.spec.11.7.1. Increase requestBodyTransmitted by bytesLength.
requestBodyTransmitted += bytesLength;
// xhr.spec.11.7.2. If not roughly 50ms have passed since these steps were last invoked, then return.
// @todo...
// xhr.spec.11.7.3. If this’s upload listener flag is set, then fire a progress event named progress at this’s upload object
// with requestBodyTransmitted and requestBodyLength.
if(requestData['uploadListenerFlag']){
requestData['upload'].dispatchEvent(new ProgressEvent('progress', {loaded: requestBodyTransmitted, total: requestBodyLength}));
}
}
// xhr.spec.11.8. Let processRequestEndOfBody be these steps:
const processRequestEndOfBody = () => {
// xhr.spec.11.8.1. Set this’s upload complete flag.
requestData['uploadCompleteFlag'] = true;
// xhr.spec.11.8.2. If this’s upload listener flag is unset, then return.
if(!requestData['uploadListenerFlag']) return;
// xhr.spec.11.8.3. Fire a progress event named progress at this’s upload object with requestBodyTransmitted and requestBodyLength.
// requestData['upload'].dispatchEvent(new ProgressEvent('progress', {loaded: requestBodyTransmitted, total: requestBodyLength}));
// xhr.spec.11.8.4. Fire a progress event named load at this’s upload object with requestBodyTransmitted and requestBodyLength.
requestData['upload'].dispatchEvent(new ProgressEvent('load', {loaded: requestBodyTransmitted, total: requestBodyLength}));
// xhr.spec.11.8.5. Fire a progress event named loadend at this’s upload object with requestBodyTransmitted and requestBodyLength.
requestData['upload'].dispatchEvent(new ProgressEvent('loadend', {loaded: requestBodyTransmitted, total: requestBodyLength}));
}
// xhr.spec.11.9. Let processResponse, given a response, be these steps:
const processResponse = (response) => {
// xhr.spec.11.9.1. Set this’s response to response.
requestData['response'] = response;
// xhr.spec.11.9.2. Handle errors for this. // @?????
handleError.call(xhr);
// xhr.spec.11.9.3. If this’s response is a network error, then return. // @?????
if(!response.statusCode){
setResponseToNetworkError.call(this);
return;
}
else
if( response.headers['location'] &&
(
response.statusCode === 301 ||
response.statusCode === 302 ||
response.statusCode === 303 ||
response.statusCode === 307 ||
response.statusCode === 308
)
){
if(requestData['_redirectCount'] >= 20){
setResponseToNetworkError.call(xhr);
handleError.call(xhr);
return;
}
requestData['_redirectCount']++;
let loc = response.headers['location'];
let parsedURL;
try{
parsedURL = new URL(loc, new URL(requestData['url']).origin);
}
catch(e){
// xhr.spec.open.6. If parsedURL is failure, then throw a "SyntaxError" DOMException
throw new DOMException(`XMLHttpRequest.open: an invalid or illegal url string was specified`, 'SyntaxError');
}
requestData['url'] = parsedURL.href;
requestData['receivedBytes'] = Buffer.alloc(0);
requestData['responseObject'] = null;
requestData['sendFlag'] = false;
requestData['async'] = true;
if(
response.statusCode >= 301 &&
response.statusCode <= 303 &&
requestData['method'].toUpperCase() !== 'GET' &&
requestData['method'].toUpperCase() !== 'HEAD'
){
requestData['method'] = 'GET';
requestData['requestBody'] = body = null;
for(let headerCase in requestData['requestHeadersCase'])
if(
headerCase === 'content-type' ||
headerCase === 'content-length' ||
headerCase === 'content-encoding' ||
headerCase === 'content-location' ||
headerCase === 'content-language'
){
let header = requestData['requestHeadersCase'][headerCase];
delete requestData['requestHeaders'][header];
delete requestData['requestHeadersCase'][headerCase];
}
}
xhr.onloadstart = null;
xhr.upload.onloadstart = null;
xhr.upload.onprogress = null;
xhr.upload.onabort = null;
xhr.upload.onerror = null;
xhr.upload.onload = null;
xhr.upload.ontimeout = null;
xhr.upload.onloadend = null;
xhr.send(body);
return;
}
requestData['status'] = response.statusCode;
requestData['statusText'] = response.statusMessage;
// xhr.spec.11.9.4. Set this’s state to headers received.
requestData['readyState'] = xhr.HEADERS_RECEIVED;
// xhr.spec.11.9.5. Fire an event named readystatechange at this.
xhr.dispatchEvent(new Event('readystatechange'));
// xhr.spec.11.9.6. If this’s state is not headers received, then return.
if(requestData['readyState'] !== xhr.HEADERS_RECEIVED) return;
// xhr.spec.11.9.7. If this’s response’s body is null, then run handle response end-of-body for this and return.
// xhr.spec.11.9.8. Let length be the result of extracting a length from this’s response’s header list.
// xhr.spec.11.9.9. If length is not an integer, then set it to 0.
let length = 0;
if(response.headers['content-length'] && !isNaN(+response.headers['content-length']))
length = +response.headers['content-length'];
// xhr.spec.11.9.10. Let processBodyChunk given bytes be these steps:
response.on('data', (chunk) => {
// xhr.spec.11.9.10.1. Append bytes to this’s received bytes.
requestData['receivedBytes'] = Buffer.concat([requestData['receivedBytes'], chunk]);
// xhr.spec.11.9.10.2. If not roughly 50ms have passed since these steps were last invoked, then return.
// @todo...
// xhr.spec.11.9.10.3. If this’s state is headers received, then set this’s state to loading.
if(requestData['readyState'] === xhr.HEADERS_RECEIVED)
requestData['readyState'] = xhr.LOADING;
// xhr.spec.11.9.10.4. Fire an event named readystatechange at this.
xhr.dispatchEvent(new Event('readystatechange'));
// xhr.spec.11.9.10.5. Fire a progress event named progress at this with this’s received bytes’s length and length.
xhr.dispatchEvent(new ProgressEvent('progress', {loaded: requestData['receivedBytes'].length, total: length}));
});
// xhr.spec.11.9.11. Let processEndOfBody be this step: run handle response end-of-body for this.
response.on('end', () => {
// xhr.spec.11.9.11.1. Handle errors for this. // @?????
handleError.call(xhr);
// xhr.spec.11.9.11.2. If xhr’s response is a network error, then return. // @?????
if(!response.statusCode) return;
// xhr.spec.11.9.11.3. Let transmitted be xhr’s received bytes’s length.
let transmitted = requestData['receivedBytes'].length;
// xhr.spec.11.9.11.4. Let length be the result of extracting a length from this’s response’s header list.
// xhr.spec.11.9.11.5. If length is not an integer, then set it to 0.
let length = 0;
if(response.headers['content-length'] && !isNaN(+response.headers['content-length']))
length = +response.headers['content-length'];
// xhr.spec.11.9.11.6. If xhr’s synchronous flag is unset, then fire a progress event named progress at xhr with transmitted and length.
// @?????
if(requestData['async'] && transmitted === 0)
xhr.dispatchEvent(new ProgressEvent('progress', {loaded: transmitted, total: length}));
// xhr.spec.11.9.11.7. Set xhr’s state to done.
requestData['readyState'] = xhr.DONE;
// xhr.spec.11.9.11.8. Unset xhr’s send() flag.
requestData['sendFlag'] = false;
// xhr.spec.11.9.11.9. Fire an event named readystatechange at xhr.
xhr.dispatchEvent(new Event('readystatechange'));
// xhr.spec.11.9.11.10. Fire a progress event named "load" at xhr with transmitted and length.
xhr.dispatchEvent(new ProgressEvent('load', {loaded: transmitted, total: length}));
// xhr.spec.11.9.11.11. Fire a progress event named loadend at xhr with transmitted and length.
xhr.dispatchEvent(new ProgressEvent('loadend', {loaded: transmitted, total: length}));
});
// xhr.spec.11.9.12. Let processBodyError be these steps:
response.on('error', (error) => {
// xhr.spec.11.9.12.1. Set this’s response to a network error.
setResponseToNetworkError.call(xhr);
// xhr.spec.11.9.12.2 Run handle errors for this.
handleError.call(xhr, error);
});
}; // @endof const processResponse = (response) => { ... }
// xhr.spec.11.10. Set this’s fetch controller to the result of fetching req ... ???????????????????????????????????????????????????
let clientRequest = null;
try{
clientRequest = requestData['fetchController'] = doClientRequest(clientRequestOptions);
}catch(err){
// xhr.spec.11.9.12.1. Set this’s response to a network error.
setResponseToNetworkError.call(xhr);
// xhr.spec.11.9.12.2 Run handle errors for this.
handleError.call(xhr, err);
return;
}
clientRequest.on('close', () => {
});
clientRequest.on('error', (err) => {
setResponseToNetworkError.call(xhr);
handleError.call(xhr, err);
});
// xhr.spec.11.7. Note: These steps are only invoked when new bytes are transmitted.
if(requestData['requestBody']){
clientRequest.setHeader('Content-Length', requestData['requestBody'].length);
(async () => {
let buffer;
if(requestData['requestBody'].source instanceof Blob){
buffer = Buffer.from (await requestData['requestBody'].source.arrayBuffer());
}
else{
buffer = await Promise.resolve(requestData['requestBody'].source);
}
clientRequest.write(buffer, processRequestBodyChunkLength); // ?????????????????????????????
// xhr.spec.11.8. Let processRequestEndOfBody be these steps:
clientRequest.end(processRequestEndOfBody);
})();
}
else{
clientRequest.end();
}
// xhr.spec.11.9. Let processResponse, given a response, be these steps:
clientRequest.on('response', processResponse);
// xhr.spec.11.11. Let now be the present time.
let now = Date.now();
// xhr.spec.11.12. Run these steps in parallel:
// xhr.spec.11.12.1. Wait until either req’s done flag is set or this’s timeout is not 0 and this’s timeout milliseconds have passed since now.
// xhr.spec.11.12.2. If req’s done flag is unset, then set this’s timed out flag and terminate this’s fetch controller.
if(requestData['timeout']){
setTimeout(() => {
if(requestData['readyState'] !== xhr.DONE){
requestData['timeoutFlag'] = true;
handleError.call(xhr);
}
}, requestData['timeout']);
}
}
// xhr.spec.12. Otherwise, if this’s synchronous flag is set:
else
{
// xhr.spec.12.1. Let <processedResponse> be false.
// xhr.spec.12.2. Let <processResponseConsumeBody>, given a <response> and <nullOrFailureOrBytes>, be these steps:
// xhr.spec.12.2.1. If <nullOrFailureOrBytes> is not failure, then set this’s response to response.
// xhr.spec.12.2.2. If <nullOrFailureOrBytes> is a byte sequence, then append nullOrFailureOrBytes to this’s received bytes.
// xhr.spec.12.2.3. Set <processedResponse> to true.
// xhr.spec.12.3. Set this’s fetch controller to the result of fetching req with <processResponseConsumeBody> set to processResponseConsumeBody
// and useParallelQueue set to true.
// xhr.spec.12.4. Let now be the present time.
// xhr.spec.12.5. Pause until either processedResponse is true or this’s timeout is not 0 and this’s timeout milliseconds have passed since now.
// xhr.spec.12.6. If processedResponse is false, then set this’s timed out flag and terminate this’s fetch controller.
// xhr.spec.12.7. Report timing for this’s fetch controller given the current global object.
// xhr.spec.12.8. Run handle response end-of-body for this.
const contentFile = '.whatwg-xhr-content-' + process.pid;
const syncFile = '.whatwg-xhr-sync-' + process.pid;
fs.writeFileSync(syncFile, '', 'utf8');
let defaultHeadersObj = defaultHeaders();
for(let name in defaultHeadersObj){
if(req_headers[name] && req_headers[name] === defaultHeadersObj[name]){
delete req_headers[name];
delete req_headersCase[name.toLowerCase()];
}
}
let headersJSON = JSON.stringify(req_headers);
let bodyFlag = body !== null && body !== '';
const execString = `
(async () => {
let fs = await import('node:fs');
let { XMLHttpRequest } = await import('${__filename}');
let xhr = new XMLHttpRequest;
let requestData = {};
xhr.onload = () => {
requestData['error'] = null;
requestData['status'] = xhr.status;
requestData['statusText'] = xhr.statusText;
requestData['response' ] = xhr.response;
requestData['responseText' ] = xhr.responseText;
requestData['responseType' ] = xhr.responseType;
requestData['responseURL' ] = xhr.responseURL;
requestData['responseXML' ] = xhr.responseXML;
requestData['responseHeaders'] = xhr.getAllResponseHeaders();
};
xhr.onerror = (e) => {
requestData['error'] = e;
}
xhr.onloadend = () => {
fs.writeFileSync('${contentFile}', JSON.stringify(requestData, null, 2), 'utf8');
fs.unlinkSync('${syncFile}');
}
xhr.open('${req_method}', '${req_url}');
let headers = JSON.parse(process.argv[1]);
for(let k in headers){
xhr.setRequestHeader(k, headers[k]);
}
if(!${bodyFlag})
xhr.send();
let body = Buffer.alloc(0);
process.stdin.on('data', (chunk) => {
body = Buffer.concat([body, chunk]);
xhr.send(body);
});
})();
`;
let syncProc = spawn(process.argv[0], ['-e', execString, headersJSON]);
if(bodyFlag && requestData['requestBody'].source instanceof Blob){
const contentBlob = '.whatwg-xhr-contentBlob-' + process.pid;
const syncBlob = '.whatwg-xhr-syncBlob-' + process.pid;
fs.writeFileSync(syncBlob, '', 'utf8');
let threadCode = `\
const fs = require('node:fs');
const threads = require('node:worker_threads');
threads.parentPort.once('message', (data) => {
data.arrayBuffer()
.then(arrayBuffer => Buffer.from(arrayBuffer))
.then(buf => {
fs.writeFileSync('${contentBlob}', buf);
fs.unlinkSync('${syncBlob}');
});
});
`;
let worker = new threads.Worker(threadCode, {eval: true});
worker.postMessage(requestData['requestBody'].source);
while(fs.existsSync(syncBlob));
requestData['requestBody'].source = fs.readFileSync(contentBlob);
fs.unlinkSync(contentBlob);
syncProc.stdin.write(requestData['requestBody'].source);
}
else
if(bodyFlag){
syncProc.stdin.write(requestData['requestBody'].source);
}
syncProc.stderr.on('data', (chunk) => {
let msg = chunk.toString();
setResponseToNetworkError.call(xhr);
handleError.call(xhr, new Error(msg));
process.exit(1);
});
while(fs.existsSync(syncFile));
syncProc.stdin.end();
processResult();
function processResult(){
let _requestData = JSON.parse(fs.readFileSync(contentFile, 'utf8')) || {};
fs.unlinkSync(contentFile);
if(_requestData['status']){
let response = {};
requestData['status'] = response.statusCode = _requestData['status'];
requestData['statusText'] = response.statusMessage = _requestData['statusText'];
requestData['responseType'] = _requestData['responseType'];
requestData['responseURL' ] = _requestData['responseURL' ];
requestData['responseXML' ] = _requestData['responseXML' ];
response.url = _requestData['responseURL'];
_requestData['responseHeaders'] = _requestData['responseHeaders'].split('\r\n');
_requestData['responseHeaders'].pop();
response.headers = {};
for(let header of _requestData['responseHeaders']){
let [name, value] = header.split('\x3A\x20');
response.headers[name] = value;
}
response.rawHeaders = [];
for(let k in response.headers){
response.rawHeaders.push(k);
response.rawHeaders.push(response.headers[k]);
}
if(_requestData['response']){
let chunk = Buffer.from(_requestData['response']);
requestData['receivedBytes'] = Buffer.concat([requestData['receivedBytes'], chunk]);
}
requestData['response'] = response;
// xhr.spec.11.9.11.7. Set xhr’s state to done.
requestData['readyState'] = xhr.DONE;
// xhr.spec.11.9.11.8. Unset xhr’s send() flag.
requestData['sendFlag'] = false;
// xhr.spec.11.9.11.9. Fire an event named readystatechange at xhr.
xhr.dispatchEvent(new Event('readystatechange'));
// xhr.spec.11.9.11.10. Fire a progress event named "load" at xhr with transmitted and length.
xhr.dispatchEvent(new ProgressEvent('load', {loaded: requestData['receivedBytes'].length, total: requestData['receivedBytes'].length}));
// xhr.spec.11.9.11.11. Fire a progress event named loadend at xhr with transmitted and length.
xhr.dispatchEvent(new ProgressEvent('loadend', {loaded: requestData['receivedBytes'].length, total: requestData['receivedBytes'].length}));
}
else{
// xhr.spec.11.9.12.1. Set this’s response to a network error.
setResponseToNetworkError.call(xhr);
// xhr.spec.11.9.12.2 Run handle errors for this.
handleError.call(xhr);
}
}
}
}; // @endof method XMLHttpRequest.prototype.send
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-getresponseheader()-method
*
* client.getResponseHeader(name)
* The getResponseHeader(name) method steps are to return the result of getting name from this’s response’s header list.
*
* @method getResponseHeader
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @param {string} name - The name of the header whose text value is required.
* @return {string|null} The header text value or null.
* @desc XMLHttpRequest method that returns the string containing the text of a particular header's value.
*/
XMLHttpRequest.prototype.getResponseHeader = function getResponseHeader(name){
// @author: validate the number of arguments
if(arguments.length < 1)
throw new TypeError(`XMLHttpRequest.getResponseHeader: At least 1 argument required, but only ${arguments.length} passed`); // firefox 102
const requestData = xmlHttpRequest.get(this);
if(!requestData['response']) return null;
if(typeof name === 'symbol')
throw new TypeError(`can't convert symbol to string`);
let headerName = String(name).toLowerCase();
let responseHeaders = {};
for(let headerName in requestData['response'].headers)
responseHeaders[headerName.toLowerCase()] = requestData['response'].headers[headerName];
if(headerName in responseHeaders && !headerName.startsWith('set-cookie'))
return responseHeaders[headerName];
return null;
}; // @endof method XMLHttpRequest.prototype.getResponseHeader
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method
*
* client.getAllResponseHeaders()
* The getAllResponseHeaders() method steps are:
* 1. Let <output> be an empty byte sequence.
* 2. Let <initialHeaders> be the result of running <sort and combine> with this’s response’s header list.
* 3. Let <headers> be the result of sorting initialHeaders in ascending order, with a being less than b if a’s name is legacy-uppercased-byte less than b’s name.
* Note: Unfortunately, this is needed for compatibility with deployed content.
* 4. For each <header> in <headers>, append <header>’s name, followed by a 0x3A 0x20 byte pair, followed by <header>’s value,
* followed by a 0x0D 0x0A byte pair, to <output>.
* 5. Return <output>.
*
* @method getAllResponseHeaders
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @return {string|null} A string representing all of the response's headers, or null if no response has been received.
* @desc XMLHttpRequest method that returns all the response headers, separated by CRLF, as a string, or null if no response.
*/
XMLHttpRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders(){
const requestData = xmlHttpRequest.get(this);
if(!requestData['response']) return '';
// xhr.spec.1. Let <output> be an empty byte sequence.
let output = '';
// xhr.spec.2. Let <initialHeaders> be the result of running <sort and combine>...
let initialHeaders = requestData['response'].headers;
// xhr.spec.3. Let <headers> be the result of sorting initialHeaders in ascending order...
let headers = Object.entries(initialHeaders);
headers.sort((a, b) => {
if(a[0].toUpperCase() > b[0].toUpperCase())
return 1;
else
return -1;
});
// xhr.spec.4. For each <header> in <headers>, append <header>’s name, followed by a 0x3A 0x20 byte pair, followed by <header>’s value,
// followed by a 0x0D 0x0A byte pair, to output.
for(let header of headers){
if(!header[0].toLowerCase().startsWith('set-cookie'))
output += header[0] + '\x3A' + '\x20' + header[1] + '\x0D' + '\x0A';
}
return output;
}; // @endof method XMLHttpRequest.prototype.getAllResponseHeaders
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#the-overridemimetype()-method
*
* client.overrideMimeType(mime)
* Acts as if the `Content-Type` header value for a response is mime. (It does not change the header.)
* Throws an "InvalidStateError" DOMException if state is loading or done.
*
* The overrideMimeType(mime) method steps are:
* 1. If this’s state is loading or done, then throw an "InvalidStateError" DOMException.
* 2. Set this’s override MIME type to the result of parsing mime.
* 3. If this’s override MIME type is failure, then set this’s override MIME type to application/octet-stream.
*
* @method overrideMimeType
* @instance
* @memberof module:whatwg-xhr.XMLHttpRequest
* @param {string} mime - MIME type to use instead of the one specified by the server.
* @desc XMLHttpRequest method that specifies a MIME type other than the one provided by the server.
* @requires module:whatwg-misc.parseMIMEType
* @requires module:whatwg-misc.serializeAMimeType
*/
XMLHttpRequest.prototype.overrideMimeType = function overrideMimeType(mime){
// @author: validate the number of arguments
if(arguments.length < 1)
throw new TypeError(`XMLHttpRequest.overrideMimeType: At least 1 argument required, but only 0 passed`); // firefox 102
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If this’s state is loading or done, then throw an "InvalidStateError" DOMException.
if(requestData['readyState'] === this.LOADING || requestData['readyState'] === this.DONE)
throw new DOMException(`XMLHttpRequest.overrideMimeType: Cannot call 'overrideMimeType()' on XMLHttpRequest after 'send()' (when its state is LOADING or DONE).`, 'InvalidStateError'); // firefox 102
let parsedMime = parseMIMEType(String(mime));
if(parsedMime !== 'failure'){
// xhr.spec.2. Set this’s override MIME type to the result of parsing mime.
requestData['overrideMimeType'] = serializeAMimeType(parsedMime);
}
else{
// xhr.spec.3. If this’s override MIME type is failure, then set this’s override MIME type to application/octet-stream.
requestData['overrideMimeType'] = 'application/octet-stream';
}
}; // @endof method XMLHttpRequest.prototype.overrideMimeType
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#interface-progressevent
*
* interface ProgressEvent : Event {
* constructor(DOMString type, optional ProgressEventInit eventInitDict = {});
*
* readonly attribute boolean lengthComputable;
* readonly attribute unsigned long long loaded;
* readonly attribute unsigned long long total;
* };
*
* dictionary ProgressEventInit : EventInit {
* boolean lengthComputable = false;
* unsigned long long loaded = 0;
* unsigned long long total = 0;
* };
*
* @class ProgressEvent
* @extends module:whatwg-xhr~Event
* @static
* @desc Interface that represents events measuring progress of an underlying process, like an HTTP request for an XMLHttpRequest client.
*/
export class ProgressEvent extends Event{
/**
* @method constructor
* @instance
* @memberof module:whatwg-xhr.ProgressEvent
* @param {string} type - A case-sensitive name of the event.
* @param {object} eventInitDict - (Optional) An object that, in addition to the properties defined in Event(), can have lengthComputable, loaded and total.
* @desc Constructs a new ProfressEvent object.
*/
constructor(type, eventInitDict = {}){
if(arguments.length < 1)
throw new TypeError(`ProgressEvent constructor: At least 1 argument required, but only 0 passed`); // firefox 102
const progressEventData = {};
progressEventData['type'] = String(type);
progressEventData['lengthComputable'] = false;
progressEventData['loaded'] = 0;
progressEventData['total'] = 0;
if(eventInitDict !== undefined && eventInitDict !== null){
if(typeof eventInitDict !== 'object' && typeof eventInitDict !== 'function')
throw new TypeError(`ProgressEvent constructor: Value can't be converted to a dictionary.`); // firefox 102
if(eventInitDict.loaded !== undefined)
progressEventData['loaded'] = isNaN(eventInitDict.loaded) ? 0 : Number(eventInitDict.loaded);
if(eventInitDict.total !== undefined)
progressEventData['total'] = isNaN(eventInitDict.total) ? 0 : Number(eventInitDict.total);
if(eventInitDict.lengthComputable !== undefined)
progressEventData['lengthComputable'] = Boolean(eventInitDict.lengthComputable);
else
progressEventData['lengthComputable'] = progressEventData['total'] !== 0;
let eventInit = {
bubbles: Boolean(eventInitDict['bubbles']),
cancelable: Boolean(eventInitDict['cancelable']),
composed: Boolean(eventInitDict['composed'])
};
super(type, eventInit);
}
else
super(type);
progressEvent.set(this, progressEventData);
}
/**
* @member {boolean} lengthComputable
* @memberof module:whatwg-xhr.ProgressEvent
* @readonly
* @default false
* @instance
* @desc A boolean value indicating if the total work to be done, and the amount of work already done, by the underlying process is calculable.
*/
get lengthComputable(){ return progressEvent.get(this)['lengthComputable']; }
/**
* @member {number} loaded
* @memberof module:whatwg-xhr.ProgressEvent
* @readonly
* @default 0
* @instance
* @desc A number representing the amount of work already performed by the underlying process.
*/
get loaded(){ return progressEvent.get(this)['loaded']; }
/**
* @member {number} total
* @memberof module:whatwg-xhr.ProgressEvent
* @readonly
* @default 0
* @instance
* @desc A number representing the total amount of work that the underlying process is in the progress of performing.
*/
get total(){ return progressEvent.get(this)['total']; }
} // @endOf class ProgressEvent extends Event
Object.defineProperty(ProgressEvent.prototype, 'lengthComputable', {enumerable: true});
Object.defineProperty(ProgressEvent.prototype, 'loaded', {enumerable: true});
Object.defineProperty(ProgressEvent.prototype, 'total', {enumerable: true});
Object.defineProperty(ProgressEvent.prototype, Symbol.toStringTag, {value: 'ProgressEvent', writable: false, enumerable: false, configurable: true});
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#formdata
*
* interface FormData {
* constructor(optional HTMLFormElement form, optional HTMLElement? submitter = null);
*
* undefined append(USVString name, USVString value);
* undefined append(USVString name, Blob blobValue, optional USVString filename);
* undefined delete(USVString name);
*
* FormDataEntryValue? get(USVString name);
* sequence<FormDataEntryValue> getAll(USVString name);
*
* boolean has(USVString name);
* undefined set(USVString name, USVString value);
* undefined set(USVString name, Blob blobValue, optional USVString filename);
*
* iterable<USVString, FormDataEntryValue>;
* };
*
* @class FormData
* @static
* @desc An interface that provides a way to construct a set of key/value pairs representing form fields and their values.
*/
export class FormData{
/**
* @method constructor
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {FormData} form - (Optional) The FormData object will be populated with the form's current keys/values.
* @param {string|Blob} value - (Optional) A submit button that is a member of the form.
* @desc Constructs a new FormData object.
*/
constructor(form, submitter){
if(arguments.length === 0 || !globalThis.HTMLFormElement){
const entryList = [];
formData.set(this, entryList);
}
else
if(globalThis.HTMLFormElement){
if(form === null || typeof form !== 'object')
throw new TypeError('FormData constructor: Argument 1 is not an object.');
if(!(form instanceof globalThis.HTMLFormElement))
throw new TypeError(' FormData constructor: Argument 1 does not implement interface HTMLFormElement.');
}
}
/**
* @method append
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {string} name - The name of the field whose data is contained in value.
* @param {string|Blob} value - The field's value, can be a string or Blob, otherwise the value is converted to a string.
* @param {string} filename - (Optional) The filename reported to the server (a string), when a Blob or File is passed as the second parameter.
* @desc Appends a new value onto an existing key inside a FormData object, or adds the key if it does not already exist.
*/
append(name, value){
// validate the arguments
if(arguments.length < 2)
throw new TypeError(`FormData.append: ${arguments.length} is not a valid argument count for any overload.`);
const entryList = formData.get(this);
const entry = {};
entry[name] = String(value);
entryList.push(entry);
}
/**
* @method delete
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {string} name - The name of the key to be deleted.
* @desc Deletes a key and its value(s) from a FormData object.
*/
delete(name){
// validate the arguments
if(arguments.length < 1)
throw new TypeError(`FormData.delete: At least 1 argument required, but only ${arguments.length} passed`);
const entryList = formData.get(this);
for(let i = 0; i < entryList.length; i++)
if(entryList[i][name] !== undefined)
entryList.splice(i--, 1);
}
/**
* @method get
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {string} name - The name of the key to be retrieved.
* @return {string|Blob} Form data entry value.
* @desc Returns the first value associated with a given key from within a FormData object.
*/
get(name){
// validate the arguments
if(arguments.length < 1)
throw new TypeError(`FormData.delete: At least 1 argument required, but only ${arguments.length} passed`);
const entryList = formData.get(this);
for(let i = 0; i < entryList.length; i++)
if(entryList[i][name] !== undefined)
return entryList[i][name];
return null;
}
/**
* @method getAll
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {string} name - The name of the key to be retrieved.
* @return {Array} An array of values whose key matches the specified name, otherwise, an empty list.
* @desc Returns all the values associated with a given key from within a FormData object.
*/
getAll(name){
// validate the arguments
if(arguments.length < 1)
throw new TypeError(`FormData.delete: At least 1 argument required, but only ${arguments.length} passed`);
const result = [];
const entryList = formData.get(this);
for(let i = 0; i < entryList.length; i++)
if(entryList[i][name] !== undefined)
result.push(entryList[i][name]);
return result;
}
/**
* @method has
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {string} name - The name of the key to be retrieved.
* @return {boolean} true if a key of FormData matches the specified name, otherwise, false.
* @desc Returns whether a FormData object contains a certain key.
*/
has(name){
// validate the arguments
if(arguments.length < 1)
throw new TypeError(`FormData.delete: At least 1 argument required, but only ${arguments.length} passed`);
const entryList = formData.get(this);
for(let i = 0; i < entryList.length; i++)
if(entryList[i][name] !== undefined)
return true;
return false;
}
/**
* @method set
* @instance
* @memberof module:whatwg-xhr.FormData
* @param {string} name - The name of the field whose data is contained in value.
* @param {string|Blob} value - The field's value, can be a string or Blob, otherwise the value is converted to a string.
* @param {string} filename - (Optional) The filename reported to the server (a string), when a Blob or File is passed as the second parameter.
* @desc Sets a new value for an existing key inside a FormData object, or adds the key/value if it does not already exist.
*/
set(name, value){
// validate the arguments
if(arguments.length < 2)
throw new TypeError(`FormData.append: ${arguments.length} is not a valid argument count for any overload.`);
const entryList = formData.get(this);
for(let i = 0; i < entryList.length; i++)
if(entryList[i][name] !== undefined)
entryList.splice(i, 1);
const entry = {};
entry[name] = String(value);
entryList.push(entry);
}
} // @endof class FormData
Object.defineProperty(FormData.prototype, Symbol.toStringTag, { value: 'FormData', writable: false, enumerable: false, configurable: true});
Object.defineProperty(FormData.prototype, Symbol.iterator, {
value: function entries(){
const entryList = formData.get(this);
let i = 0;
return{
next(){
return i < entryList.length ?
{ value: Object.entries(entryList[i++])[0], done: false} :
{ value: undefined, done: true};
},
[Symbol.iterator](){
return this;
}
}
},
writable: false, enumerable: false, configurable: true
});
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#handle-errors
*
* To handle errors for an XMLHttpRequest object xhr, run these steps:
* 1. If xhr’s send() flag is unset, then return.
* 2. If xhr’s timed out flag is set, then run the request error steps for xhr, "timeout", and "TimeoutError" DOMException.
* 3. Otherwise, if xhr’s response’s aborted flag is set, run the request error steps for xhr, "abort", and "AbortError" DOMException.
* 4. Otherwise, if xhr’s response is a network error, then run the request error steps for xhr, error, and "NetworkError" DOMException.
*
* @func handleError
* @this module:whatwg-xhr.XMLHttpRequest
* @param {object} Error object.
* @desc Handle errors for an XMLHttpRequest object, as given by {@link https://xhr.spec.whatwg.org/#handle-errors XHR specs}.
*/
function handleError(error){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If xhr’s send() flag is unset, then return.
if(!requestData['sendFlag']){
return;
}
// xhr.spec.2. If xhr’s timed out flag is set, then run the request error steps for xhr, "timeout", and "TimeoutError" DOMException.
if(requestData['timeoutFlag']){
requestErrorSteps.call(this, 'timeout', new DOMException('Timeout error', 'TimeoutError'));
}
else
// xhr.spec.3. If xhr’s response’s aborted flag is set, run the request error steps for xhr, "abort", and "AbortError" DOMException.
if(requestData['response'] && requestData['_responseAbort']){
requestErrorSteps.call(this, 'abort', new DOMException('Abort error', 'AbortError'));
}
else
// xhr.spec.4. if xhr’s response is a network error, then run the request error steps for xhr, "error", and "NetworkError" DOMException.
if(requestData['response'] === ''){
requestErrorSteps.call(this, 'error', new DOMException('Network error', 'NetworkError'));
}
} // @endof function handleError
/**
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#request-error-steps
*
* The request error steps for an XMLHttpRequest object <xhr>, <event>, and optionally <exception> are:
* 1. Set <xhr>’s state to done.
* 2. Unset <xhr>’s send() flag.
* 3. Set <xhr>’s response to a network error.
* 4. If <xhr>’s synchronous flag is set, then throw <exception>.
* 5. Fire an event named readystatechange at <xhr>.
* Note: At this point it is clear that <xhr>’s synchronous flag is unset.
* 6. If <xhr>’s upload complete flag is unset, then:
* 1. Set <xhr>’s upload complete flag.
* 2. If <xhr>’s upload listener flag is set, then:
* 1. Fire a progress event named <event> at <xhr>’s upload object with 0 and 0.
* 2. Fire a progress event named 'loadend' at <xhr>’s upload object with 0 and 0.
* 7. Fire a progress event named <event> at <xhr> with 0 and 0.
* 8. Fire a progress event named 'loadend' at <xhr> with 0 and 0.
*
* @func requestErrorSteps
* @this module:whatwg-xhr.XMLHttpRequest
* @param {string} Event type
* @param {object|string} Exception
* @desc Request error steps for an XMLHttpRequest object <xhr>, <event>, and optionally <exception>, as given by {@link https://xhr.spec.whatwg.org/#request-error-steps XHR specs}.
*/
function requestErrorSteps(event, exception){
const xhr = this;
const requestData = xmlHttpRequest.get(xhr);
// xhr.spec.1. Set <xhr>’s state to done.
requestData['readyState'] = xhr.DONE;
// xhr.spec.2. Unset <xhr>’s send() flag.
requestData['sendFlag'] = false;
// xhr.spec.3. Set <xhr>’s response to a network error.
setResponseToNetworkError.call(xhr);
// xhr.spec.4. If <xhr>’s synchronous flag is set, then throw <exception>.
if(!requestData['async']){
throw exception;
}
// xhr.spec.5. Fire an event named readystatechange at <xhr>.
xhr.dispatchEvent(new Event('readystatechange'));
// xhr.spec.6. If <xhr>’s upload complete flag is unset, then:
if(!requestData['uploadCompleteFlag']){
// xhr.spec.6.1. Set <xhr>’s upload complete flag.
requestData['uploadCompleteFlag'] = true;
// xhr.spec.6.2. If <xhr>’s upload listener flag is set, then:
if(requestData['uploadListenerFlag']){
const upload = requestData['upload'];
// xhr.spec.6.2.1. Fire a progress event named <event> at <xhr>’s upload object with 0 and 0.
upload.dispatchEvent(new ProgressEvent(event, {loaded: 0, total: 0}));
// xhr.spec.6.2.2. Fire a progress event named 'loadend' at <xhr>’s upload object with 0 and 0.
upload.dispatchEvent(new ProgressEvent('loadend', {loaded: 0, total: 0}));
}
}
// xhr.spec.7. Fire a progress event named <event> at <xhr> with 0 and 0.
xhr.dispatchEvent(new ProgressEvent(event, {loaded: 0, total: 0}));
// xhr.spec.7. Fire a progress event named 'loadend' at <xhr> with 0 and 0.
xhr.dispatchEvent(new ProgressEvent('loadend', {loaded: 0, total: 0}));
} // @endof function requestErrorSteps
/**
* Fetch Living Standard — Last Updated 17 June 2024
* url: https://fetch.spec.whatwg.org/#concept-network-error
*
* A network error is a response whose type is "error", status is 0, status message is the empty byte sequence, header list is « »,
* body is null, and body info is a new response body info.
*
* @func setResponseToNetworkError
* @this module:whatwg-xhr.XMLHttpRequest
* @desc Network error is a response whose type is "error", as defined by {@link https://fetch.spec.whatwg.org/#concept-network-error Fetch specs}.
*/
function setResponseToNetworkError(){
const requestData = xmlHttpRequest.get(this);
if(requestData['fetchController']){
requestData['fetchController'].destroy();
requestData['fetchController'] = null;
}
requestData['response'] = '';
// requestData['status'] = 0;
// requestData['statusText'] = '';
requestData['requestHeaders' ] = {};
requestData['requestHeadersCase'] = {};
requestData['receivedBytes'] = Buffer.alloc(0);
requestData['_responseBody'] = null;
} // @endof function setResponseToNetworkError
/*
* XMLHttpRequest Living Standard — Last Updated 20 February 2023
* url: https://xhr.spec.whatwg.org/#response-body
*
* 3.6.6. Response body
*
* To get a response MIME type for an XMLHttpRequest object xhr, run these steps:
* 1. Let mimeType be the result of <extracting a MIME type> from xhr’s response’s header list.
* 2. If mimeType is failure, then set mimeType to text/xml.
* 3. Return mimeType.
*
* To get a final MIME type for an XMLHttpRequest object xhr, run these steps:
* 1. If xhr’s <override MIME type> is null, return the result of get a response MIME type for xhr.
* 2. Return xhr’s override MIME type.
*
* To get a final encoding for an XMLHttpRequest object xhr, run these steps:
* 1. Let label be null.
* 2. Let responseMIME be the result of <get a response MIME type> for xhr.
* 3. If responseMIME’s parameters["charset"] exists, then set label to it.
* 4. If xhr’s override MIME type’s parameters["charset"] exists, then set label to it.
* 5. If label is null, then return null.
* 6. Let encoding be the result of getting an encoding from label.
* 7. If encoding is failure, then return null.
* 8. Return encoding.
* The above steps intentionally do not use the get a final MIME type as it would not be web compatible.
*
* To set a document response for an XMLHttpRequest object xhr, run these steps:
* 1. If xhr’s response’s body is null, then return.
* 2. Let finalMIME be the result of get a final MIME type for xhr.
* 3. If finalMIME is not an HTML MIME type or an XML MIME type, then return.
* 4. If xhr’s response type is the empty string and finalMIME is an HTML MIME type, then return.
* Note: This is restricted to xhr’s response type being "document" in order to prevent breaking legacy content.
* 5. If finalMIME is an HTML MIME type, then:
* 1. Let charset be the result of get a final encoding for xhr.
* 2. If charset is null, prescan the first 1024 bytes of xhr’s received bytes and if that does not terminate
* unsuccessfully then let charset be the return value.
* 3. If charset is null, then set charset to UTF-8.
* 4. Let document be a document that represents the result parsing xhr’s received bytes
* following the rules set forth in the HTML Standard for an HTML parser with scripting
* disabled and a known definite encoding charset. [HTML]
* 5. Flag document as an HTML document.
* 6. Otherwise, let document be a document that represents the result of running the XML parser
* with XML scripting support disabled on xhr’s received bytes.
* If that fails (unsupported character encoding, namespace well-formedness error, etc.),
* then return null. [HTML]
* Note: Resources referenced will not be loaded and no associated XSLT will be applied.
* 7. If charset is null, then set charset to UTF-8.
* 8. Set document’s encoding to charset.
* 9. Set document’s content type to finalMIME.
* 10. Set document’s URL to xhr’s response’s URL.
* 11. Set document’s origin to xhr’s relevant settings object’s origin.
* 12. Set xhr’s response object to document.
*
* To get a text response for an XMLHttpRequest object xhr, run these steps:
* 1. If xhr’s response’s body is null, then return the empty string.
* 2. Let charset be the result of get a final encoding for xhr.
* 3. If xhr’s response type is the empty string, charset is null, and the result of get a final MIME type for xhr is an XML MIME type,
* then use the rules set forth in the XML specifications to determine the encoding. Let charset be the determined encoding. [XML] [XML-NAMES]
* This is restricted to xhr’s response type being the empty string to keep the non-legacy response type value "text" simple.
* 4. If charset is null, then set charset to UTF-8.
* 5. Return the result of running decode on xhr’s received bytes using fallback encoding charset.
* Authors are strongly encouraged to always encode their resources using UTF-8.
*/
/**
* @func getResponseMimeType
* @this module:whatwg-xhr.XMLHttpRequest
* @return {string} MIME type.
* @desc Get a response MIME type for an XMLHttpRequest object xhr, as given by {@link https://xhr.spec.whatwg.org/#response-body XHR specs}.
* @requires module:helper-content-type.parse
*/
function getResponseMimeType(){
const requestData = xmlHttpRequest.get(this);
const response = requestData['response'];
let mimeType = '';
// xhr.spec.1. Let mimeType be the result of <extracting a MIME type> from xhr’s response’s header list.
try{
let obj = contentTypeParse(response.headers['content-type']);
mimeType = response.headers['content-type'];
}
// xhr.spec.2. If mimeType is failure, then set mimeType to text/xml.
catch(e){
mimeType = 'text/xml';
}
// xhr.spec.3. Return mimeType.
return mimeType;
} // @endof function getResponseMimeType
/**
* @func getFinalMimeType
* @this module:whatwg-xhr.XMLHttpRequest
* @return {string} MIME type.
* @desc Get a final MIME type for an XMLHttpRequest object xhr, as given by {@link https://xhr.spec.whatwg.org/#response-body XHR specs}.
*/
function getFinalMimeType(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If xhr’s <override MIME type> is null, return the result of get a response MIME type for xhr.
if(requestData['overrideMimeType'] === null)
return getResponseMimeType.call(this);
// xhr.spec.2. Return xhr’s override MIME type.
return requestData['overrideMimeType'];
} // @endof function getFinalMimeType
/**
* @func getFinalEncoding
* @this module:whatwg-xhr.XMLHttpRequest
* @return {string|null} Final encoding.
* @desc Get a final encoding for an XMLHttpRequest object xhr, as given by {@link https://xhr.spec.whatwg.org/#response-body XHR specs}.
* @requires module:helper-content-type.parse
*/
function getFinalEncoding(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. Let label be null.
let label = null;
// xhr.spec.2. Let responseMIME be the result of <get a response MIME type> for xhr.
let responseMIME = getResponseMimeType.call(this);
let obj = null;
// xhr.spec.3. If responseMIME’s parameters["charset"] exists, then set label to it.
try{
obj = contentTypeParse(responseMIME);
if(obj.parameters && obj.parameters.charset)
label = obj.parameters.charset;
}catch(e){}
// xhr.spec.4. If xhr’s override MIME type’s parameters["charset"] exists, then set label to it.
if(requestData['overrideMimeType']){
try{
obj = contentTypeParse(requestData['overrideMimeType']);
if(obj.parameters && obj.parameters.charset)
label = obj.parameters.charset;
}catch(e){}
}
// xhr.spec.5. If label is null, then return null.
if(label === null) return null;
// xhr.spec.6. Let encoding be the result of getting an encoding from label.
let encoding = whatwgEncoding.labelToName(label);
// xhr.spec.7. If encoding is failure, then return null.
// xhr.spec.8. Return encoding.
return encoding;
} // @endof function getFinalEncoding
/**
* @func setDocumentResponse
* @this module:whatwg-xhr.XMLHttpRequest
* @desc Set a document response for an XMLHttpRequest object xhr, as given by {@link https://xhr.spec.whatwg.org/#response-body XHR specs}.
* @requires module:helper-content-type.parse
*/
function setDocumentResponse(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If xhr’s response’s body is null, then return.
if(requestData['receivedBytes'].length === 0) return;
// xhr.spec.2. Let <finalMIME> be the result of get a final MIME type for xhr.
let finalMIME = getFinalMimeType.call(this);
let obj;
try{
obj = contentTypeParse(finalMIME);
// xhr.spec.3. If <finalMIME> is not an HTML MIME type or an XML MIME type, then return.
if(
obj.type !== 'text/xml' &&
obj.type !== 'application/xml' &&
!obj.type.endsWith('+xml') &&
obj.type !== 'text/html'
)
return;
// xhr.spec.4. If xhr’s response type is the empty string and <finalMIME> is an HTML MIME type, then return.
if(obj.type === 'text/html' && this.responseType === '')
return;
// xhr.spec.5. If finalMIME is an HTML MIME type, then:
if(obj.type === 'text/html'){
// xhr.spec.5.1. Let <charset> be the result of get a final encoding for xhr.
let charset = getFinalEncoding.call(this);
// xhr.spec.5.2. If <charset> is null, prescan the first 1024 bytes of xhr’s received bytes and if that does not terminate
// unsuccessfully then let <charset> be the return value.
if(charset === null){
charset = htmlEncodingSniffer(requestData['receivedBytes'].subarray(0, 1024));
}
// xhr.spec.5.3. If <charset> is null, then set <charset> to UTF-8.
if(charset === null) charset = 'UTF-8';
// xhr.spec.5.4. Let <document> be a document that represents the result parsing xhr’s received bytes
// following the rules set forth in the HTML Standard for an HTML parser with scripting
// disabled and a known definite encoding charset. [HTML]
// xhr.spec.5.5. Flag document as an HTML document.
// @todo...
}
else{
// xhr.spec.6. Otherwise, let document be a document that represents the result of running the XML parser
// with XML scripting support disabled on xhr’s received bytes.
// If that fails (unsupported character encoding, namespace well-formedness error, etc.),
// then return null. [HTML]
// Note: Resources referenced will not be loaded and no associated XSLT will be applied.
// @todo...
}
}catch(e){
return;
}
// xhr.spec.7. If charset is null, then set charset to UTF-8.
// xhr.spec.8. Set document’s encoding to charset.
// xhr.spec.9. Set document’s content type to finalMIME.
// xhr.spec.10. Set document’s URL to xhr’s response’s URL.
// xhr.spec.11. Set document’s origin to xhr’s relevant settings object’s origin.
// @todo...
// xhr.spec.12. Set xhr’s response object to document.
requestData['responseObject'] = null;
} // @endof function setDocumentResponse
/**
* @func getTextResponse
* @this module:whatwg-xhr.XMLHttpRequest
* @return {string} Text response.
* @desc Get a text for an XMLHttpRequest object xhr, as given by {@link https://xhr.spec.whatwg.org/#response-body XHR specs}.
* @requires module:helper-content-type.parse
* @requires module:helper-xml-encoding-sniffer.xmlEncodingSniffer
*/
function getTextResponse(){
const requestData = xmlHttpRequest.get(this);
// xhr.spec.1. If xhr’s response’s body is null, then return the empty string.
// author. if(requestData['_responseBody'] === null)
if(requestData['receivedBytes'].length === 0){
return '';
}
// xhr.spec.2. Let charset be the result of get a final encoding for xhr.
let charset = getFinalEncoding.call(this);
// xhr.spec.3. If xhr’s response type is the empty string, charset is null, and the result of get a final MIME type for xhr is an XML MIME type,
// then use the rules set forth in the XML specifications to determine the encoding. Let charset be the determined encoding. [XML] [XML-NAMES]
// This is restricted to xhr’s response type being the empty string to keep the non-legacy response type value "text" simple.
if(requestData['responseType'] === '' && charset === null){
let finalMimeType = getFinalMimeType.call(this);
try{
let obj = contentTypeParse(finalMimeType);
if(obj.type === 'text/xml' || obj.type === 'application/xml' || obj.type.endsWith('+xml')){
// @todo: use the rules set forth in the XML specifications to determine the encoding. Let charset be the determined encoding.
charset = xmlEncodingSniffer(requestData['receivedBytes']);
}
}catch(e){}
}
// xhr.spec.4. If charset is null, then set charset to UTF-8.
if(charset === null) charset = 'UTF-8';
// xhr.spec.5. Return the result of running decode on xhr’s received bytes using fallback encoding charset.
return whatwgEncoding.decode(requestData['receivedBytes'], charset);
} // @endof function getTextResponse
/**
* @class ClientRequestData
* @desc Emulates the node http.ClientRequest for processing data url's.
*/
class ClientRequestData{
/**
* @method constructor
* @instance
* @memberof module:whatwg-xhr~ClientRequestData
* @param {object} response - Emulates the http.IncomingMessage object.
* @return {object} ClientRequestData object.
* @desc Constructs a new ClientRequestData object.
*/
constructor(response){
this.response = response;
}
/**
* @method destroy
* @instance
* @memberof module:whatwg-xhr~ClientRequestData
* @return {object} ClientRequestData object.
* @desc Emulates the node http.ClientRequest destroy() method.
*/
destroy(){
return this;
}
/**
* @method end
* @instance
* @memberof module:whatwg-xhr~ClientRequestData
* @param {function} callback - Callback function to invoke.
* @return {object} ClientRequestData object.
* @desc Emulates the node http.ClientRequest end() method.
*/
end(callback){
if(typeof callback === 'function')
callback();
return this;
}
/**
* @method on
* @instance
* @memberof module:whatwg-xhr~ClientRequestData
* @param {string} type - The event to invoke.
* @param {function} callback - Callback function to invoke.
* @return {object} ClientRequestData object.
* @desc Emulates the node http.ClientRequest on() method.
*/
on(type, callback){
if(type === 'response' && typeof callback === 'function')
callback(this.response);
else
if(type === 'error' && typeof callback === 'function' && this.response.statusCode === 0)
callback(new ProgressEvent('error'));
return this;
}
} // @endof class ClientRequestData
/**
* @func requestDataURL
* @param {object} options - Options passed to the node http.request function.
* @return {object} ClientRequestData object.
* @desc Emulates the node http.request function for processing data url's.
* @requires module:whatwg-misc.dataURLProcessor
* @requires module:whatwg-misc.serializeAMimeType
*/
function requestDataURL(options){
let urlStruct = null;
let responseDataURL = null;
try{
urlStruct = dataURLProcessor(new URL(options.url));
}catch(e){
urlStruct = 'failure';
}
if(urlStruct !== 'failure'){
if(options.method.toUpperCase() === 'HEAD')
responseDataURL = Readable.from(Buffer.alloc(0));
else
responseDataURL = Readable.from(Buffer.from(urlStruct.body));
responseDataURL.statusCode = 200;
responseDataURL.statusMessage = 'OK';
responseDataURL.url = options.url;
responseDataURL.headers = {
'Content-Type': serializeAMimeType(urlStruct.mimeType),
}
}
else{
responseDataURL = Readable.from(Buffer.alloc(0));
responseDataURL.statusCode = 0;
responseDataURL.statusMessage = '';
responseDataURL.headers = {};
}
return new ClientRequestData(responseDataURL);
} // @endof function requestDataURL
Source: lib/whatwg-xhr.js
A Node.js implementation of the WHATWG XMLHttpRequest Living Standard for non-browser environments.