UNPKG

jsdom

Version:

A JavaScript implementation of many web standards

1,088 lines (952 loc) 36.8 kB
"use strict"; const { Worker, receiveMessageOnPort, MessageChannel } = require("node:worker_threads"); const { inspect } = require("util"); const { parseURL, serializeURL } = require("whatwg-url"); const { getBOMEncoding, labelToName, legacyHookDecode } = require("@exodus/bytes/encoding.js"); const tough = require("tough-cookie"); const { MIMEType } = require("whatwg-mimetype"); const DOMException = require("../../../generated/idl/DOMException"); const idlUtils = require("../../../generated/idl/utils"); const Document = require("../../../generated/idl/Document"); const Blob = require("../../../generated/idl/Blob"); const FormData = require("../../../generated/idl/FormData"); const XMLHttpRequestUpload = require("../../../generated/idl/XMLHttpRequestUpload"); const ProgressEvent = require("../../../generated/idl/ProgressEvent"); const { isHeaderName, isHeaderValue, normalizeHeaderValue } = require("../fetch/header-utils"); const HeaderList = require("../fetch/header-list"); const { isForbiddenRequestHeader } = require("../fetch/header-types"); const { performFetch, isNetworkError } = require("./xhr-utils"); const XMLHttpRequestEventTargetImpl = require("./XMLHttpRequestEventTarget-impl").implementation; const { parseIntoDocument } = require("../../browser/parser"); const { fragmentSerialization } = require("../domparsing/serialization"); const { copyToArrayBufferInTargetRealmDestructively, concatTypedArrays } = require("../helpers/binary-data"); const { setupForSimpleEventAccessors } = require("../helpers/create-event-accessor"); const { utf8Encode, utf8Decode } = require("../helpers/encoding"); const { fireAnEvent } = require("../helpers/events"); const { parseJSONFromBytes } = require("../helpers/json"); const { asciiCaseInsensitiveMatch } = require("../helpers/strings"); const { serializeEntryList } = require("./multipart-form-data"); const syncWorkerFile = require.resolve("./xhr-sync-worker.js"); // TODO: simplify after dropping Node.js v20 support function trimUint8Array(arr, newSize) { if (arr.buffer.transfer) { return new Uint8Array(arr.buffer.transfer(newSize)); } return arr.slice(0, newSize); } let syncWorker = null; function getSyncWorker() { if (!syncWorker) { syncWorker = new Worker(syncWorkerFile); syncWorker.unref(); syncWorker.on("exit", () => { syncWorker = null; }); } return syncWorker; } const SYNC_WORKER_TIMEOUT_MS = 2 * 60 * 1000; // How long to wait for the worker to acknowledge it received our message. This detects the race // where the worker called process.exit() (idle timeout) but the main thread hasn't processed the // exit event yet — so getSyncWorker() returns a dead worker and postMessage() silently succeeds // but the message is never delivered. const SYNC_WORKER_ACK_TIMEOUT_MS = 5000; // Sends a serialized XHR request to the persistent worker thread and synchronously blocks // until it responds. Uses a two-phase protocol: // // 1. Ack phase: the worker writes 1 to sharedBuffer[0] immediately upon receiving the message. // If this times out, the worker is assumed dead and we retry with a fresh worker. // 2. Done phase: the worker writes 1 to sharedBuffer[1] after posting the response to the // MessagePort. After waking, receiveMessageOnPort() synchronously reads the response. function sendSyncWorkerRequest(config) { const result = trySendSyncWorkerRequest(config, SYNC_WORKER_ACK_TIMEOUT_MS); if (result !== null) { return result; } // Ack timed out — worker likely dead from idle timeout race. Terminate and retry. syncWorker?.terminate(); syncWorker = null; const retryResult = trySendSyncWorkerRequest(config, SYNC_WORKER_TIMEOUT_MS); if (retryResult !== null) { return retryResult; } throw new Error("Sync XHR worker timed out"); } function trySendSyncWorkerRequest(config, ackTimeout) { const sharedBuffer = new SharedArrayBuffer(8); const int32 = new Int32Array(sharedBuffer); const { port1, port2 } = new MessageChannel(); getSyncWorker().postMessage({ sharedBuffer, responsePort: port2, config }, [port2]); // Phase 1: wait for the worker to acknowledge receipt. const ackResult = Atomics.wait(int32, 0, 0, ackTimeout); if (ackResult === "timed-out") { return null; } // Phase 2: wait for the actual response. const waitResult = Atomics.wait(int32, 1, 0, SYNC_WORKER_TIMEOUT_MS); if (waitResult === "timed-out") { throw new Error("Sync XHR worker timed out"); } const msg = receiveMessageOnPort(port1); if (!msg) { throw new Error("No response received from sync XHR worker"); } return msg.message; } const READY_STATES = Object.freeze({ UNSENT: 0, OPENED: 1, HEADERS_RECEIVED: 2, LOADING: 3, DONE: 4 }); const tokenRegexp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; const allowedRequestMethods = new Set(["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE"]); const forbiddenRequestMethods = new Set(["TRACK", "TRACE", "CONNECT"]); // Helper functions for error handling function dispatchError(xhr, errMessage) { // Store the error message for sync XHR worker to serialize xhr._error = errMessage; requestErrorSteps(xhr, "error", DOMException.create(xhr._globalObject, [errMessage, "NetworkError"])); } function requestErrorSteps(xhr, event, exception) { const { upload } = xhr; xhr.readyState = READY_STATES.DONE; xhr._send = false; setResponseToNetworkError(xhr); if (xhr._synchronous) { throw exception; } fireAnEvent("readystatechange", xhr); if (!xhr._uploadComplete) { xhr._uploadComplete = true; if (xhr._uploadListener) { fireAnEvent(event, upload, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false }); fireAnEvent("loadend", upload, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false }); } } fireAnEvent(event, xhr, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false }); fireAnEvent("loadend", xhr, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false }); } function setResponseToNetworkError(xhr) { xhr._responseBytes = xhr._responseCache = xhr._responseTextCache = xhr._responseXMLCache = null; xhr._responseHeaders = new HeaderList(); xhr.status = 0; xhr.statusText = ""; } class XMLHttpRequestImpl extends XMLHttpRequestEventTargetImpl { constructor(window) { super(window); const { _ownerDocument } = this; this.upload = XMLHttpRequestUpload.createImpl(window); // Public WebIDL properties this.readyState = READY_STATES.UNSENT; this.responseURL = ""; this.status = 0; this.statusText = ""; // Request configuration this._synchronous = false; this._withCredentials = false; this._mimeType = null; this._auth = null; this._method = undefined; this._responseType = ""; this._requestHeaders = new HeaderList(); this._referrer = _ownerDocument.URL; this._url = ""; this._timeout = 0; this._body = undefined; this._preflight = false; this._overrideMIMEType = null; this._overrideCharset = null; this._requestManager = _ownerDocument._requestManager; this._dispatcher = window._dispatcher; this._cookieJar = _ownerDocument._cookieJar; this._encoding = _ownerDocument._encoding; this._origin = window._origin; this._userAgent = window.navigator.userAgent; // Runtime/response state this._beforeSend = false; this._send = false; this._controller = null; this._timeoutStart = 0; this._timeoutId = 0; this._timeoutFn = null; this._responseBytes = null; this._responseCache = null; this._responseTextCache = null; this._responseXMLCache = null; this._responseHeaders = new HeaderList(); this._filteredResponseHeaders = new Set(); this._error = ""; this._uploadComplete = false; this._uploadListener = false; // Signifies that we're calling abort() from xhr-utils.js because of a window shutdown. // In that case the termination reason is "fatal", not "end-user abort". this._abortError = false; this._bufferStepSize = 1 * 1024 * 1024; // pre-allocate buffer increase step size. init value is 1MB this._totalReceivedChunkSize = 0; } get responseType() { return this._responseType; } set responseType(responseType) { if (this.readyState === READY_STATES.LOADING || this.readyState === READY_STATES.DONE) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState === READY_STATES.OPENED && this._synchronous) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } this._responseType = responseType; } get response() { if (this._responseCache) { // Needed because of: https://github.com/jsdom/webidl2js/issues/149 return idlUtils.tryWrapperForImpl(this._responseCache); } let res; const responseBytes = this._responseBytes?.slice(0, this._totalReceivedChunkSize) ?? null; switch (this.responseType) { case "": case "text": { res = this.responseText; break; } case "arraybuffer": { if (!responseBytes) { return null; } res = copyToArrayBufferInTargetRealmDestructively(responseBytes.buffer, this._globalObject); break; } case "blob": { if (!responseBytes) { return null; } const contentType = finalMIMEType(this); res = Blob.createImpl(this._globalObject, [ [new Uint8Array(responseBytes)], { type: contentType || "" } ]); break; } case "document": { res = this.responseXML; break; } case "json": { if (this.readyState !== READY_STATES.DONE || !responseBytes) { res = null; break; } try { res = parseJSONFromBytes(responseBytes); } catch { res = null; } break; } } this._responseCache = res; // Needed because of: https://github.com/jsdom/webidl2js/issues/149 return idlUtils.tryWrapperForImpl(res); } get responseText() { if (this.responseType !== "" && this.responseType !== "text") { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState !== READY_STATES.LOADING && this.readyState !== READY_STATES.DONE) { return ""; } if (this._responseTextCache) { return this._responseTextCache; } const responseBytes = this._responseBytes?.slice(0, this._totalReceivedChunkSize) ?? null; if (!responseBytes) { return ""; } const fallbackEncodingLabel = finalCharset(this) || getBOMEncoding(responseBytes) || "UTF-8"; const res = legacyHookDecode(responseBytes, fallbackEncodingLabel); this._responseTextCache = res; return res; } get responseXML() { if (this.responseType !== "" && this.responseType !== "document") { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState !== READY_STATES.DONE) { return null; } if (this._responseXMLCache) { return this._responseXMLCache; } const responseBytes = this._responseBytes?.slice(0, this._totalReceivedChunkSize) ?? null; if (!responseBytes) { return null; } const contentType = finalMIMEType(this); let isHTML = false; const parsed = MIMEType.parse(contentType); if (parsed) { isHTML = parsed.isHTML(); const isXML = parsed.isXML(); if (!isXML && !isHTML) { return null; } } if (this.responseType === "" && isHTML) { return null; } const encoding = finalCharset(this) || labelToName(getBOMEncoding(responseBytes)) || "UTF-8"; const resText = legacyHookDecode(responseBytes, encoding); if (!resText) { return null; } const res = Document.createImpl(this._globalObject, [], { options: { url: this._url, lastModified: new Date(this._responseHeaders.get("last-modified")), parsingMode: isHTML ? "html" : "xml", cookieJar: { setCookieSync: () => undefined, getCookieStringSync: () => "" }, encoding, parseOptions: this._ownerDocument._parseOptions } }); try { parseIntoDocument(resText, res); } catch { this._responseXMLCache = null; return null; } res.close(); this._responseXMLCache = res; return res; } get timeout() { return this._timeout; } set timeout(val) { if (this._synchronous) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } this._timeout = val; clearTimeout(this._timeoutId); if (val > 0 && this._timeoutFn) { this._timeoutId = setTimeout( this._timeoutFn, Math.max(0, val - ((new Date()).getTime() - this._timeoutStart)) ); } else { this._timeoutFn = null; this._timeoutStart = 0; } } get withCredentials() { return this._withCredentials; } set withCredentials(val) { if (!(this.readyState === READY_STATES.UNSENT || this.readyState === READY_STATES.OPENED)) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this._send) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } this._withCredentials = val; } abort() { // Terminate the request clearTimeout(this._timeoutId); this._timeoutFn = null; this._timeoutStart = 0; if (this._controller) { this._controller.abort(); this._controller = null; } if (this._abortError) { // Special case that ideally shouldn't be going through the public API at all. // Run the https://xhr.spec.whatwg.org/#handle-errors "fatal" steps. this.readyState = READY_STATES.DONE; this._send = false; setResponseToNetworkError(this); return; } if ((this.readyState === READY_STATES.OPENED && this._send) || this.readyState === READY_STATES.HEADERS_RECEIVED || this.readyState === READY_STATES.LOADING) { requestErrorSteps(this, "abort"); } if (this.readyState === READY_STATES.DONE) { this.readyState = READY_STATES.UNSENT; setResponseToNetworkError(this); } } getAllResponseHeaders() { if (this.readyState === READY_STATES.UNSENT || this.readyState === READY_STATES.OPENED) { return ""; } const result = []; for (const [key, value] of this._responseHeaders) { const lcKey = key.toLowerCase(); if (!this._filteredResponseHeaders.has(lcKey)) { result.push(`${lcKey}: ${value}`); } } return result.join("\r\n"); } getResponseHeader(header) { if (this.readyState === READY_STATES.UNSENT || this.readyState === READY_STATES.OPENED) { return null; } const lcHeader = header.toLowerCase(); if (this._filteredResponseHeaders.has(lcHeader)) { return null; } return this._responseHeaders.get(lcHeader); } open(method, url, asynchronous, user, password) { const { _ownerDocument } = this; if (!_ownerDocument) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (!tokenRegexp.test(method)) { throw DOMException.create(this._globalObject, [ "The string did not match the expected pattern.", "SyntaxError" ]); } const upperCaseMethod = method.toUpperCase(); if (forbiddenRequestMethods.has(upperCaseMethod)) { throw DOMException.create(this._globalObject, ["The operation is insecure.", "SecurityError"]); } if (this._controller && typeof this._controller.abort === "function") { this._controller.abort(); } if (allowedRequestMethods.has(upperCaseMethod)) { method = upperCaseMethod; } if (typeof asynchronous !== "undefined") { this._synchronous = !asynchronous; } else { this._synchronous = false; } if (this._responseType && this._synchronous) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } if (this._synchronous && this._timeout) { throw DOMException.create(this._globalObject, [ "The object does not support the operation or argument.", "InvalidAccessError" ]); } this._method = method; const urlRecord = parseURL(url, { baseURL: _ownerDocument.baseURL() }); if (!urlRecord) { throw DOMException.create(this._globalObject, [ "The string did not match the expected pattern.", "SyntaxError" ]); } if (user || (password && !urlRecord.username)) { this._auth = { user, pass: password }; urlRecord.username = ""; urlRecord.password = ""; } this._url = serializeURL(urlRecord); this._requestHeaders = new HeaderList(); this._preflight = false; this._send = false; this._uploadListener = false; this._body = undefined; this._abortError = false; this._responseBytes = null; this._responseCache = null; this._responseTextCache = null; this._responseXMLCache = null; this._responseHeaders = new HeaderList(); this._totalReceivedChunkSize = 0; this.responseURL = ""; this.status = 0; this.statusText = ""; readyStateChange(this, READY_STATES.OPENED); } overrideMimeType(mime) { if (this.readyState === READY_STATES.LOADING || this.readyState === READY_STATES.DONE) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } this._overrideMIMEType = "application/octet-stream"; // Waiting for better spec: https://github.com/whatwg/xhr/issues/157 const parsed = MIMEType.parse(mime); if (parsed) { this._overrideMIMEType = parsed.essence; const charset = parsed.parameters.get("charset"); if (charset) { this._overrideCharset = labelToName(charset); } } } // TODO: Add support for URLSearchParams and ReadableStream send(body) { const { upload, _ownerDocument } = this; // Not per spec, but per tests: https://github.com/whatwg/xhr/issues/65 if (!_ownerDocument) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } if (this.readyState !== READY_STATES.OPENED || this._send) { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } this._beforeSend = true; try { if (this._method === "GET" || this._method === "HEAD") { body = null; } if (body !== null) { let extractedContentType = null; if (Document.isImpl(body)) { // Note: our utf8Encode() does both USVString conversion and UTF-8 encoding. this._body = utf8Encode(fragmentSerialization(body, { requireWellFormed: false })); } else { const { body: extractedBody, type } = extractBody(body); this._body = extractedBody; extractedContentType = type; } const originalAuthorContentType = this._requestHeaders.get("content-type"); if (originalAuthorContentType !== null) { if (Document.isImpl(body) || typeof body === "string") { const parsed = MIMEType.parse(originalAuthorContentType); if (parsed) { const charset = parsed.parameters.get("charset"); if (charset && !asciiCaseInsensitiveMatch(charset, "UTF-8")) { parsed.parameters.set("charset", "UTF-8"); this._requestHeaders.set("Content-Type", parsed.toString()); } } } } else if (Document.isImpl(body)) { if (body._parsingMode === "html") { this._requestHeaders.set("Content-Type", "text/html;charset=UTF-8"); } else { this._requestHeaders.set("Content-Type", "application/xml;charset=UTF-8"); } } else if (extractedContentType !== null) { this._requestHeaders.set("Content-Type", extractedContentType); } } } finally { if (this._beforeSend) { this._beforeSend = false; } else { throw DOMException.create(this._globalObject, ["The object is in an invalid state.", "InvalidStateError"]); } } if (Object.keys(upload._eventListeners).length > 0) { this._uploadListener = true; } // request doesn't like zero-length bodies if (this._body && this._body.byteLength === 0) { this._body = null; } // Per XHR spec step 11: "If req's body is null, then set this's upload complete flag." // This prevents upload events from firing for GET/HEAD and other bodyless requests. // Note: this._body may be undefined (for GET/HEAD) or null (for zero-length bodies). if (!this._body) { this._uploadComplete = true; } if (this._synchronous) { this._adoptSerializedResponse(sendSyncWorkerRequest(this._serializeRequest())); this.readyState = READY_STATES.LOADING; if (this._error) { dispatchError(this, this._error); throw DOMException.create(this._globalObject, [this._error, "NetworkError"]); } else { const contentLength = this._responseHeaders.get("content-length") || "0"; const byteLength = parseInt(contentLength, 10) || this._responseBytes.length; const progressObj = { lengthComputable: false }; if (byteLength !== 0) { progressObj.total = byteLength; progressObj.loaded = byteLength; progressObj.lengthComputable = true; } fireAnEvent("progress", this, ProgressEvent, progressObj); readyStateChange(this, READY_STATES.DONE); fireAnEvent("load", this, ProgressEvent, progressObj); fireAnEvent("loadend", this, ProgressEvent, progressObj); } } else { this._send = true; this._totalReceivedChunkSize = 0; this._bufferStepSize = 1 * 1024 * 1024; if (body !== null && body !== "") { this._uploadComplete = false; } else { this._uploadComplete = true; } // State for upload progress - use this._body which is the processed Uint8Array const uploadTotal = this._body ? this._body.byteLength : 0; const uploadProgress = { lengthComputable: uploadTotal > 0, total: uploadTotal, loaded: 0 }; // Create abort controller BEFORE firing loadstart so open() called in // loadstart handler can properly abort this request const abortController = new AbortController(); this._controller = abortController; // Register with request manager so window.close()/stop() can abort this request const requestManagerEntry = { abort: () => { this._abortError = true; abortController.abort(); } }; if (this._requestManager) { this._requestManager.add(requestManagerEntry); } // Per XHR spec, fire loadstart on xhr first, then on upload. fireAnEvent("loadstart", this, ProgressEvent); if (!this._uploadComplete && this._uploadListener) { fireAnEvent("loadstart", upload, ProgressEvent, uploadProgress); } // Per XHR spec: "If this's state is not opened or this's send() flag is unset, return." // Also check if this request was aborted (e.g., by open() called in loadstart handler) if (this.readyState !== READY_STATES.OPENED || !this._send || abortController.signal.aborted) { if (this._requestManager) { this._requestManager.remove(requestManagerEntry); } return; } // Async fetch and body streaming (async () => { try { const response = await performFetch( this._dispatcher, { url: this._url, method: this._method, requestHeaders: this._requestHeaders, body: this._body, origin: this._origin, referrer: this._referrer, userAgent: this._userAgent, withCredentials: this._withCredentials, auth: this._auth, cookieJar: this._cookieJar, uploadListener: this._uploadListener }, abortController.signal ); // Handle network errors (includes CORS failures) if (isNetworkError(response)) { if (abortController.signal.aborted) { // Request was aborted - don't fire error events return; } dispatchError(this, response.error?.message || "Network error"); return; } // Fire upload complete events if (!this._uploadComplete) { this._uploadComplete = true; if (this._uploadListener) { uploadProgress.loaded = uploadProgress.total; fireAnEvent("progress", upload, ProgressEvent, uploadProgress); fireAnEvent("load", upload, ProgressEvent, uploadProgress); fireAnEvent("loadend", upload, ProgressEvent, uploadProgress); } } // Process response headers (CORS filtering done by performFetch) const { headers, filteredResponseHeaders } = response; this.responseURL = response.url; this.status = response.status; this.statusText = response.statusText; this._responseHeaders = headers; this._filteredResponseHeaders = filteredResponseHeaders; // Set up progress tracking // If content-encoding is set, the body was compressed and we report decompressed bytes, // so lengthComputable must be false (method b from the XHR spec) const contentEncoding = headers.get("content-encoding"); const contentLength = headers.get("content-length") || "0"; const bufferLength = parseInt(contentLength, 10) || 0; const progressObj = { lengthComputable: false, loaded: 0, total: 0 }; if (bufferLength !== 0 && !contentEncoding) { progressObj.total = bufferLength; progressObj.lengthComputable = true; } // Pre-allocate buffer this._responseBytes = new Uint8Array(this._bufferStepSize); this._responseCache = null; this._responseTextCache = null; this._responseXMLCache = null; readyStateChange(this, READY_STATES.HEADERS_RECEIVED); // Track progress for deduplication let lastProgressReported; // Stream the response body if (response.body) { for await (const chunk of response.body) { // Check if aborted if (abortController.signal.aborted) { break; } // Store decompressed bytes this._totalReceivedChunkSize += chunk.length; if (this._totalReceivedChunkSize >= this._bufferStepSize) { this._bufferStepSize *= 2; while (this._totalReceivedChunkSize >= this._bufferStepSize) { this._bufferStepSize *= 2; } const tmpBuf = new Uint8Array(this._bufferStepSize); tmpBuf.set(this._responseBytes); this._responseBytes = tmpBuf; } this._responseBytes.set(chunk, this._totalReceivedChunkSize - chunk.length); this._responseCache = null; this._responseTextCache = null; this._responseXMLCache = null; if (this.readyState === READY_STATES.HEADERS_RECEIVED) { this.readyState = READY_STATES.LOADING; } fireAnEvent("readystatechange", this); progressObj.loaded = this._totalReceivedChunkSize; if (lastProgressReported !== progressObj.loaded) { lastProgressReported = progressObj.loaded; fireAnEvent("progress", this, ProgressEvent, progressObj); } } } // Request complete - trim the pre-allocated buffer to actual size (zero-copy when available) if (this._responseBytes) { this._responseBytes = trimUint8Array(this._responseBytes, this._totalReceivedChunkSize); } clearTimeout(this._timeoutId); this._timeoutFn = null; this._timeoutStart = 0; this._controller = null; if (this._requestManager) { this._requestManager.remove(requestManagerEntry); } // Don't fire completion events if aborted if (abortController.signal.aborted) { return; } // Fire final progress if not already fired with this loaded value if (lastProgressReported !== progressObj.loaded) { fireAnEvent("progress", this, ProgressEvent, progressObj); } readyStateChange(this, READY_STATES.DONE); fireAnEvent("load", this, ProgressEvent, progressObj); fireAnEvent("loadend", this, ProgressEvent, progressObj); } catch (err) { this._controller = null; if (this._requestManager) { this._requestManager.remove(requestManagerEntry); } // Don't fire error events if aborted if (!abortController.signal.aborted) { dispatchError(this, err.message || String(err)); } } })(); if (this.timeout > 0) { this._timeoutStart = (new Date()).getTime(); this._timeoutFn = () => { this._controller?.abort(); if (!(this.readyState === READY_STATES.UNSENT || (this.readyState === READY_STATES.OPENED && !this._send) || this.readyState === READY_STATES.DONE)) { this._send = false; let stateChanged = false; if (!this._uploadComplete) { fireAnEvent("progress", upload, ProgressEvent); readyStateChange(this, READY_STATES.DONE); fireAnEvent("timeout", upload, ProgressEvent); fireAnEvent("loadend", upload, ProgressEvent); stateChanged = true; } fireAnEvent("progress", this, ProgressEvent); if (!stateChanged) { readyStateChange(this, READY_STATES.DONE); } fireAnEvent("timeout", this, ProgressEvent); fireAnEvent("loadend", this, ProgressEvent); } this.readyState = READY_STATES.UNSENT; }; this._timeoutId = setTimeout(this._timeoutFn, this.timeout); } } } setRequestHeader(header, value) { if (this.readyState !== READY_STATES.OPENED) { throw DOMException.create( this._globalObject, ["setRequestHeader() can only be called in the OPENED state.", "InvalidStateError"] ); } if (this._send) { throw DOMException.create( this._globalObject, ["setRequestHeader() cannot be called after send()", "InvalidStateError"] ); } value = normalizeHeaderValue(value); if (!isHeaderName(header)) { throw DOMException.create(this._globalObject, ["Invalid header name", "SyntaxError"]); } if (!isHeaderValue(value)) { throw DOMException.create(this._globalObject, ["Invalid header value", "SyntaxError"]); } if (isForbiddenRequestHeader(header, value)) { return; } this._requestHeaders.combine(header, value); } // Serialization methods for sync XHR worker communication // Called in main process before spawning sync worker _serializeRequest() { return { method: this._method, url: this._url, body: this._body, requestHeaders: this._requestHeaders.toJSON(), withCredentials: this._withCredentials, mimeType: this._mimeType, auth: this._auth, responseType: this._responseType, timeout: this._timeout, preflight: this._preflight, cookieJar: this._cookieJar.serializeSync(), encoding: this._encoding, origin: this._origin, referrer: this._referrer, userAgent: this._userAgent }; } // Called in main process after sync worker returns _adoptSerializedResponse(response) { this.status = response.status; this.statusText = response.statusText; this.responseURL = response.responseURL; if (response.responseBytes) { this._responseBytes = trimUint8Array(response.responseBytes, response.totalReceivedChunkSize); this._totalReceivedChunkSize = response.totalReceivedChunkSize; } this._responseHeaders = HeaderList.fromJSON(response.responseHeaders); this._filteredResponseHeaders = response.filteredResponseHeaders; this._error = response.error || ""; this._uploadComplete = response.uploadComplete; if (response.cookieJar) { this._cookieJar = tough.CookieJar.deserializeSync( response.cookieJar, this._ownerDocument._cookieJar.store ); } } // Called in worker to set up XHR from serialized config _adoptSerializedRequest(config) { this._method = config.method; this._url = config.url; this._body = config.body; this._requestHeaders = HeaderList.fromJSON(config.requestHeaders); this._synchronous = false; // Run as async in worker this._withCredentials = config.withCredentials; this._mimeType = config.mimeType; this._auth = config.auth; this._responseType = config.responseType; this._timeout = config.timeout; this._preflight = config.preflight; this._cookieJar = config.cookieJar ? tough.CookieJar.deserializeSync(config.cookieJar) : null; this._encoding = config.encoding; this._origin = config.origin; this._referrer = config.referrer; this._userAgent = config.userAgent; this.readyState = READY_STATES.OPENED; } // Called in worker to serialize response _serializeResponse() { let error = this._error; if (error && typeof error !== "string") { error = error.stack || inspect(error); } return { status: this.status, statusText: this.statusText, responseURL: this.responseURL, responseBytes: this._responseBytes, totalReceivedChunkSize: this._totalReceivedChunkSize, responseHeaders: this._responseHeaders.toJSON(), filteredResponseHeaders: this._filteredResponseHeaders, error, uploadComplete: this._uploadComplete, cookieJar: this._cookieJar.serializeSync() }; } } setupForSimpleEventAccessors(XMLHttpRequestImpl.prototype, ["readystatechange"]); function readyStateChange(xhr, readyState) { if (xhr.readyState === readyState) { return; } xhr.readyState = readyState; fireAnEvent("readystatechange", xhr); } function finalMIMEType(xhr) { return xhr._overrideMIMEType || xhr._responseHeaders.get("content-type"); } function finalCharset(xhr) { if (xhr._overrideCharset) { return xhr._overrideCharset; } const parsedContentType = MIMEType.parse(xhr._responseHeaders.get("content-type")); if (parsedContentType) { return labelToName(parsedContentType.parameters.get("charset")); } return null; } function extractBody(bodyInit) { // https://fetch.spec.whatwg.org/#concept-bodyinit-extract // We represent the body as a `Uint8Array`. if (Blob.isImpl(bodyInit)) { return { body: bodyInit._bytes, type: bodyInit.type === "" ? null : bodyInit.type }; } else if (idlUtils.isArrayBuffer(bodyInit)) { return { body: new Uint8Array(bodyInit).slice(0), type: null }; } else if (ArrayBuffer.isView(bodyInit)) { return { body: new Uint8Array(bodyInit), type: null }; } else if (FormData.isImpl(bodyInit)) { const { boundary, outputChunks } = serializeEntryList(bodyInit._entries); return { body: concatTypedArrays(outputChunks), type: "multipart/form-data; boundary=" + utf8Decode(boundary) }; } // Must be a string return { body: utf8Encode(bodyInit), type: "text/plain;charset=UTF-8" }; } exports.implementation = XMLHttpRequestImpl;