Repository: mjwwit/node-XMLHttpRequest
Pull request #31: sync policy, MIME override, encoding support, and more
Author: login: Bhpsngum
Created: 2025-05-08T13:35:35Z | Merged: 2025-05-17T07:52:20Z

## Synchronous policy and fixes
- Add new non-standard option `syncPolicy: "warn" | "enabled" | "disabled"` (default is "warn") to explicitly enable/disable synchronous XHR with or without warning
	- `"warn"`: enable synchronous xhr but shows a warning when the user attempts to call `xhr.open()` with sync flag:
		> [Deprecation] Synchronous XMLHttpRequest is deprecated because of its detrimental effects to the end user's experience. For more information, see https://xhr.spec.whatwg.org/#sync-flag
	- `"enabled"`: enable synchronous xhr, no warnings
	- `"disabled"`: disable synchronous, calling `xhr.open()` with sync flag won't show any warning, but an exception is thrown when calling `xhr.send()` afterwards:
		> Synchronous requests are disabled for this instance.
- Fix problem with data not encoded properly when using synchronous request (#29)
- Sync responses now write data into OS temporary directory instead of cwd (since rwx permissions can be altered for cwd)
- If file system operations fail, an error message will be thrown:
	> Synchronous operation aborted: Unable to access the OS temporary directory for read/write operations.
## Encoding and MIME type support
- Add `xhr.overrideMimeType(type)` (#27)
- Add support for encodings other than utf-8 (can be read from `Content-Type` header or through user's custom encoding via `xhr.overrideMimeType()`)
## Non-standard methods
- Removed `xhr.setDisableHeaderCheck()` and instead added a non-standard `disableHeaderCheck: true | false` (default is `false`)
- Add deprecation message for `xhr.getRequestHeader()`:
	> `xhr.getRequestHeader()` is deprecated and will be removed in a future release. It’s non-standard and not part of the XHR spec.
## Others
- Prevent `XMLHttpRequest` to be called without `new` operator
- Unifies `createFileOrSyncResponse` and `createResponse` into one method `createResponse`
- Brings some methods back to private scope to prevent tampering (especially the `readyState` value)
- Add immutable flags value UNSENT, OPENED, HEADERS_RECEIVED, LOADING, and DONE to both `XMLHttpRequest` and any newly-created xhr object
- Add two more options, `textDecoder: <func>` for users to specify custom text decoder (to support more range of encodings for old Node versions) and `xmlParser: <func>` to support parsing for `xhr.responseXML`


Diff:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 555b1b7..541b904 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,16 +4,18 @@ on:
     branches: [ master ]
   pull_request:
     branches: [ master ]
+  workflow_dispatch:
 jobs:
   integration-tests:
     runs-on: ubuntu-latest
     strategy:
+      fail-fast: false
       matrix:
-        node-version: [12.x, 20.x, 22.x, 23.x]
+        node-version: [13.x, 20.x, 22.x, 23.x]
     steps:
       - uses: actions/checkout@v3
       - name: Use Node.js ${{ matrix.node-version }}
         uses: actions/setup-node@v3
         with:
           node-version: ${{ matrix.node-version }}
-      - run: npm test
\ No newline at end of file
+      - run: npm test
diff --git a/README.md b/README.md
index c6c7912..0b5babb 100644
--- a/README.md
+++ b/README.md
@@ -13,19 +13,94 @@ XHR object.
 Note: use the lowercase string "xmlhttprequest-ssl" in your require(). On
 case-sensitive systems (eg Linux) using uppercase letters won't work.
 
-### Non-standard features ###
-
-Non-standard options for this module are passed through the `XMLHttpRequest` constructor. The following options control `https:` SSL requests: `ca`, `cert`, `ciphers`, `key`, `passphrase`, `pfx`, and `rejectUnauthorized`. You can find their functionality in the [Node.js docs](https://nodejs.org/api/https.html#httpsrequestoptions-callback).
-
-Additionally, the `agent` option allows you to specify a [Node.js Agent](https://nodejs.org/api/https.html#class-httpsagent) instance, allowing connection reuse.
-
-To prevent a process from not exiting naturally because a request socket from this module is still open, you can set `autoUnref` to a truthy value.
-
-This module allows control over the maximum number of redirects that are followed. You can set the `maxRedirects` option to do this. The default number is 20.
-
-Using the `allowFileSystemResources` option allows you to control access to the local filesystem through the `file:` protocol.
-
-The `origin` option allows you to set a base URL for the request. The resulting request URL will be constructed as follows `new URL(url, origin)`.
+## Non-standard features ##
+### Additional options ###
+
+Non-standard options for this module are passed through the `XMLHttpRequest` constructor. Here is the list of all options:
+
+<table>
+    <thead>
+        <tr>
+            <th>Option</th>
+            <th>Default value</th>
+            <th>Description</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td><code>ca</code></td>
+            <td rowspan="6"><code>undefined</code></td>
+            <td rowspan="7">Control <code>https:</code> requests, you can find their functionality in the <a href="https://nodejs.org/api/https.html#httpsrequestoptions-callback">Nodejs Documentation</a></td>
+        </tr>
+        <tr>
+            <td><code>cert</code></td>
+        </tr>
+        <tr>
+            <td><code>ciphers</code></td>
+        </tr>
+		<tr>
+            <td><code>key</code></td>
+        </tr>
+		<tr>
+            <td><code>passhphrase</code></td>
+        </tr>
+		<tr>
+            <td><code>pfx</code></td>
+        </tr>
+		<tr>
+            <td><code>rejectUnauthorized</code></td>
+			<td><code>true</code></td>
+        </tr>
+		<tr>
+            <td><code>agent</code></td>
+			<td><code>undefined</code></td>
+			<td>Allows to specify a <a href="https://nodejs.org/api/https.html#class-httpsagent">Nodejs Agent</a> instance, allowing connection reuse</td>
+        </tr>
+		<tr>
+            <td><code>autoUnref</code></td>
+			<td><code>false</code></td>
+			<td>Set to any truthy value to prevent a process from not exiting naturally because a request socket from this module is still open</td>
+        </tr>
+		<tr>
+            <td><code>maxRedirects</code></td>
+			<td><code>20</code></td>
+			<td>Allows control over the maximum number of redirects that are followed</td>
+        </tr>
+		<tr>
+            <td><code>allowFileSystemResources</code></td>
+			<td><code>true</code></td>
+			<td>Allows control access to the local filesystem through the <code>file:</code> protocol</td>
+        </tr>
+		<tr>
+            <td><code>origin</code></td>
+			<td><code>undefined</code></td>
+			<td>Set a base URL for the requests called using this instance. The resulting request URL will be constructed as follows: <code>new URL(url, origin)</code></td>
+        </tr>
+		<tr>
+            <td><code>syncPolicy</code></td>
+			<td><code>"warn"</code></td>
+			<td>Control feature behavior of the synchronous feature:<ul><li><code>"disabled"</code>: Disable the feature completely, throws error after calling <code>.send()</code> if in synchronous mode</li><li><code>"warn"</code>: Enable the feature, but show a warning when calling <code>.open()</code> with synchronous mode</li><li><code>"enabled"</code>: Enable the feature without showing any additional warnings or errors</li></ul></td>
+        </tr>
+		<tr>
+            <td><code>disableHeaderCheck</code></td>
+			<td><code>false</code></td>
+			<td>Disable the check against forbidden headers to be added to a XHR request</td>
+        </tr>
+		<tr>
+            <td><code>xmlParser</code></td>
+			<td>none</td>
+			<td>Specify a parser (non-async) to parse document from text when <code>xhr.responseType</code> is either <code>"document"</code> or in text format. If the parser is invalid or omitted, <code>xhr.responseXML</code> will be <code>null</code></td>
+        </tr>
+		<tr>
+            <td><code>textDecoder</code></td>
+			<td><code>TextDecoder</code> or <code>buf.toString(enc)</code> depending on Node version</td>
+			<td>Specify a text decoder (non-async), accepting a buffer <code>buf</code> and encoding <code>enc</code> to decode to desired encoding.<br>Note that <code>TextDecoder</code> at version 12 does not support a wide range of encodings than later node version does</td>
+        </tr>
+    </tbody>
+</table>
+
+### Additional methods ###
+`XMLHttpRequest` object created using this library exposes `xhr.getRequestHeader(header_name)` method to retrieve any header content by name in the request headers list. This feature is deprecated and will be removed in future releases.
 
 # Original README #
 
diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js
index 8bbc904..f37edf0 100644
--- a/lib/XMLHttpRequest.js
+++ b/lib/XMLHttpRequest.js
@@ -12,7 +12,31 @@
  */
 
 var fs = require('fs');
+var os = require('os');
+var path = require('path');
 var spawn = require('child_process').spawn;
+/**
+ * Constants
+ */
+
+var stateConstants = {
+  UNSENT: 0,
+  OPENED: 1,
+  HEADERS_RECEIVED: 2,
+  LOADING: 3,
+  DONE: 4
+};
+
+var assignStateConstants = function (object) {
+  for (let stateKey in stateConstants) Object.defineProperty(object, stateKey, {
+    enumerable: true,
+    writable: false,
+    configurable: false,
+    value: stateConstants[stateKey]
+  });
+}
+
+assignStateConstants(XMLHttpRequest);
 
 /**
  * Module exports.
@@ -36,6 +60,40 @@ XMLHttpRequest.XMLHttpRequest = XMLHttpRequest;
 function XMLHttpRequest(opts) {
   "use strict";
 
+  if (!new.target) {
+    throw new TypeError("Failed to construct 'XMLHttpRequest': Please use the 'new' operator, this object constructor cannot be called as a function.");
+  }
+
+  var dataMap = Object.create(null);
+  
+  /**
+   * Safely assign any key with value to an object, preventing prototype pollution
+   * @param {any} obj Object to assign
+   * @param {any} key key name
+   * @param {any} value value to assign
+   * @param {boolean} assignable whether user can change this value (this defaults to `true` when value is a function)
+   */
+  var assignProp = function (obj, key, value, assignable) {
+    if ("function" === typeof value) Object.defineProperty(obj, key, {
+      value: value,
+      writable: true,
+      enumerable: true,
+      configurable: true
+    });
+    else if (assignable) Object.defineProperty(obj, key, {
+      get: function () { return dataMap[key]; },
+      set: function (value) { dataMap[key] = value; },
+      enumerable: true,
+      configurable: true
+    });
+    else Object.defineProperty(obj, key, {
+      get: function () { return dataMap[key]; },
+      set: undefined,
+      enumerable: true,
+      configurable: true
+    });
+  }
+
   // defines a list of default options to prevent parameters pollution
   var default_options = {
     pfx: undefined,
@@ -49,10 +107,33 @@ function XMLHttpRequest(opts) {
     agent: undefined,
     allowFileSystemResources: true,
     maxRedirects: 20, // Chrome standard
+    syncPolicy: "warn",
+    disableHeaderCheck: false,
+    xmlParser: function (text) {
+      return null;
+    },
+    textDecoder: function (buf, enc) {
+      if ("function" === typeof TextDecoder) try {
+        return new TextDecoder(enc).decode(buf);
+      }
+      catch (e) {}
+      return buf.toString(enc);
+    },
     origin: undefined
   };
 
-  opts = Object.assign(default_options, opts);
+  opts = Object.assign(Object.create(null), default_options, opts);
+
+  if (opts.syncPolicy !== "warn" && opts.syncPolicy !== "disabled" && opts.syncPolicy !== "enabled") {
+    opts.syncPolicy = "warn";
+  }
+
+  for (var i of ["xmlParser", "textDecoder"]) {
+    if (typeof opts[i] !== "function") {
+      //@TODO: find a reliable way to check if function is async
+      opts[i] = default_options[i];
+    }
+  }
 
   var sslOptions = {
     pfx: opts.pfx,
@@ -82,11 +163,9 @@ function XMLHttpRequest(opts) {
   var response;
 
   // Request settings
-  var settings = {};
+  var settings = Object.create(null);
 
-  // Disable header blacklist.
-  // Not part of XHR specs.
-  var disableHeaderCheck = false;
+  assignStateConstants(this);
 
   // Set some default headers
   var defaultHeaders = {
@@ -94,7 +173,7 @@ function XMLHttpRequest(opts) {
     "Accept": "*/*"
   };
 
-  var headers = Object.assign({}, defaultHeaders);
+  var headers = Object.assign(Object.create(null), defaultHeaders);
 
   // These headers are not user setable.
   // The following are allowed but banned in the spec:
@@ -135,36 +214,35 @@ function XMLHttpRequest(opts) {
   var errorFlag = false;
   var abortedFlag = false;
 
-  // Event listeners
-  var listeners = {};
+  // Custom encoding (if user called via xhr.overrideMimeType)
+  var customEncoding = "";
 
-  /**
-   * Constants
-   */
+  // Event listeners
+  var listeners = Object.create(null);
 
-  this.UNSENT = 0;
-  this.OPENED = 1;
-  this.HEADERS_RECEIVED = 2;
-  this.LOADING = 3;
-  this.DONE = 4;
+  // private ready state (not exposed so user cannot modify)
+  var readyState = this.UNSENT;
 
   /**
    * Public vars
    */
 
-  // Current state
-  this.readyState = this.UNSENT;
+  Object.defineProperty(this, "readyState", {
+    get: function () { return readyState; },
+    configurable: true,
+    enumerable: true
+  });
 
   // default ready state change handler in case one is not set or is set late
-  this.onreadystatechange = null;
+  assignProp(this, 'onreadystatechange', null, true);
 
   // Result & response
-  this.responseText = "";
-  this.responseXML = "";
-  this.responseURL = "";
-  this.response = Buffer.alloc(0);
-  this.status = null;
-  this.statusText = null;
+  assignProp(this, 'responseText', "");
+  assignProp(this, "responseXML", "");
+  assignProp(this, "responseURL", "");
+  assignProp(this, "response", Buffer.alloc(0));
+  assignProp(this, "status", null);
+  assignProp(this, "statusText", null);
 
   // xhr.responseType is supported:
   //   When responseType is 'text' or '', self.responseText will be utf8 decoded text.
@@ -173,7 +251,7 @@ function XMLHttpRequest(opts) {
   //   When responseType is 'arraybuffer', self.response is an ArrayBuffer.
   //   When responseType is 'blob', self.response is a Blob.
   // cf. section 3.6, subsections 8,9,10,11 of https://xhr.spec.whatwg.org/#the-response-attribute
-  this.responseType = ""; /* 'arraybuffer' or 'text' or '' or 'json' or 'blob' */
+  assignProp(this, "responseType", "", true); /* 'arraybuffer' or 'text' or '' or 'json' or 'blob' */
 
   /**
    * Private methods
@@ -186,7 +264,7 @@ function XMLHttpRequest(opts) {
    * @return boolean False if not allowed, otherwise true
    */
   var isAllowedHttpHeader = function(header) {
-    return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1);
+    return opts.disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1);
   };
 
   /**
@@ -199,52 +277,6 @@ function XMLHttpRequest(opts) {
     return (method && forbiddenRequestMethods.indexOf(method) === -1);
   };
 
-  /**
-   * When xhr.responseType === 'arraybuffer', xhr.response must have type ArrayBuffer according
-   * to section 3.6.9 of https://xhr.spec.whatwg.org/#the-response-attribute .
-   * However, bufTotal = Buffer.concat(...) often has byteOffset > 0, so bufTotal.buffer is larger
-   * than the useable region in bufTotal. This means that a new copy of bufTotal would need to be
-   * created to get the correct ArrayBuffer. Instead, do the concat by hand to create the right
-   * sized ArrayBuffer in the first place.
-   *
-   * The return type is Uint8Array,
-   * because often Buffer will have Buffer.length < Buffer.buffer.byteLength.
-   *
-   * @param {Array<Buffer>} bufferArray
-   * @returns {Uint8Array}
-   */
-  var concat = function(bufferArray) {
-    let length = 0, offset = 0;
-    for (let k = 0; k < bufferArray.length; k++)
-      length += bufferArray[k].length;
-    const result = new Uint8Array(length);
-    for (let k = 0; k < bufferArray.length; k++)
-    {
-      result.set(bufferArray[k], offset);
-      offset += bufferArray[k].length;
-    }
-    return result;
-  };
-
-  /**
-   * When xhr.responseType === 'arraybuffer', xhr.response must have type ArrayBuffer according
-   * to section 3.6.9 of https://xhr.spec.whatwg.org/#the-response-attribute .
-   * However, buf = Buffer.from(str) often has byteOffset > 0, so buf.buffer is larger than the
-   * usable region in buf. This means that a new copy of buf would need to be created to get the
-   * correct arrayBuffer. Instead, do it by hand to create the right sized ArrayBuffer in the
-   * first place.
-   *
-   * @param {string} str
-   * @returns {Buffer}
-   */
-  var stringToBuffer = function(str) {
-    const ab = new ArrayBuffer(str.length)
-    const buf = Buffer.from(ab);
-    for (let k = 0; k < str.length; k++)
-      buf[k] = Number(str.charCodeAt(k));
-    return buf;
-  }
-
   /**
    * Given a Buffer buf, check whether buf.buffer.byteLength > buf.length and if so,
    * create a new ArrayBuffer whose byteLength is buf.length, containing the bytes.
@@ -264,10 +296,120 @@ function XMLHttpRequest(opts) {
     return ab;
   }
 
+  /**
+   * Given the user-input (or Content-Type header value) of MIME type,
+   * Parse given string to retrieve mimeType and its encoding (defaults to utf8 if not exists)
+   * @param {string} contentType
+   */
+  var parseContentType = function (contentType) {
+    const regex = /([a-zA-Z0-9!#$%&'*+.^_`|~-]+\/[a-zA-Z0-9!#$%&'*+.^_`|~-]+)(?:; charset=([a-zA-Z0-9-]+))?/;
+  
+    const matches = contentType.toLowerCase().match(regex);
+  
+    if (matches) {
+      const mimeType = matches[1];
+      const charset = matches[2] || 'utf-8';
+  
+      return { mimeType, charset };
+    } else {
+      return { mimeType: "", charset: "utf-8" }
+    }
+  }
+
+  /**
+   * Called when an error is encountered to deal with it.
+   * @param  status  {number}    HTTP status code to use rather than the default (0) for XHR errors.
+   */
+  var handleError = function(error, status) {
+    dataMap.status = status || 0;
+    dataMap.statusText = error.message || "";
+    dataMap.responseText = "";
+    dataMap.responseXML = "";
+    dataMap.responseURL = "";
+    dataMap.response = Buffer.alloc(0);
+    errorFlag = true;
+    setState(self.DONE);
+    if (!settings.async) throw error;
+  };
+
+  /**
+   * Construct the correct form of response, given default content type
+   *
+   * The input is the response parameter which is a Buffer.
+   * When self.responseType is "", "text",
+   *   the input is further refined to be: new TextDecoder(encoding).decode(response),
+   *   encoding is defined either by `Content-Type` header or set through `xhr.overrideMimetype()`.
+   * When self.responseType is "json",
+   *   the input is further refined to be: JSON.parse(response.toString('utf8')).
+   * When self.responseType is "arraybuffer", "blob",
+   *   the input is further refined to be: checkAndShrinkBuffer(response).
+   * A special case is when self.responseType is "document",
+   *   the decoded text will be passed to a parser function to create a DOM, or returns `null`
+   *
+   * @param {Buffer} response
+   */
+  var createResponse = function(response, customContentType) {
+    dataMap.responseText = null;
+    dataMap.responseXML = null;
+    switch (self.responseType) {
+      case 'json':
+        dataMap.response = JSON.parse(response.toString('utf8'));
+        break;
+      case 'blob':
+      case 'arraybuffer':
+        // When self.responseType === 'arraybuffer', self.response is an ArrayBuffer.
+        // Get the correct sized ArrayBuffer.
+        dataMap.response = checkAndShrinkBuffer(response);
+        if (dataMap.responseType === 'blob' && typeof Blob === 'function') {
+          // Construct the Blob object that contains response.
+          dataMap.response = new Blob([self.response]);
+        }
+        break;
+      default:
+        try {
+          dataMap.responseText = opts.textDecoder.call(opts, response, customEncoding || parseContentType(String(customContentType)).charset);
+        }
+        catch (e) {
+          // fall back to utf8 ONLY if custom encoding is present
+          if (customEncoding) dataMap.responseText = response.toString('utf8');
+          else dataMap.responseText = "";
+        }
+        dataMap.response = self.responseText;
+        try { dataMap.responseXML = opts.xmlParser.call(opts, self.responseText); }
+        catch (e) { dataMap.responseXML = null; }
+    }
+
+    // Special handling of self.responseType === 'document'
+    if (dataMap.responseType === 'document') {
+      dataMap.response = self.responseXML;
+      dataMap.responseText = null;
+    }
+  }
+
   /**
    * Public methods
    */
 
+  /**
+   * Acts as if the Content-Type header value for a response is mime. (It does not change the header.) 
+   * Throws an error if state is LOADING or DONE.
+   * 
+   * @param {string} mimeType - The MIME type to override with (e.g., "text/plain; charset=UTF-8").
+   */
+  assignProp(this, 'overrideMimeType', function(mimeType) {
+    if (arguments.length === 0) {
+      throw new TypeError("Failed to execute 'overrideMimeType' on 'XMLHttpRequest': 1 argument required, but only 0 present.");
+    }
+
+    // check if state is LOADING or DONE
+    if (readyState === this.LOADING || readyState === this.DONE) {
+      throw new Error("INVALID_STATE_ERR: MimeType cannot be overridden when the state is LOADING or DONE.");
+    }
+
+    // parse mimeType from given string and set custom charset
+    customEncoding = parseContentType(String(mimeType)).charset;
+  });
+
   /**
    * Open the connection. Currently supports local server requests.
    *
@@ -277,8 +419,8 @@ function XMLHttpRequest(opts) {
    * @param string user Username for basic authentication (optional)
    * @param string password Password for basic authentication (optional)
    */
-  this.open = function(method, url, async, user, password) {
-    this.abort();
+  assignProp(this, 'open', function(method, url, async, user, password) {
+    abort();
     errorFlag = false;
     abortedFlag = false;
 
@@ -295,6 +437,11 @@ function XMLHttpRequest(opts) {
       "password": password || null
     };
 
+    // check for sync
+    if (opts.syncPolicy === "warn" && !settings.async) {
+      console.warn("[Deprecation] Synchronous XMLHttpRequest is deprecated because of its detrimental effects to the end user's experience. For more information, see https://xhr.spec.whatwg.org/#sync-flag");
+    }
+
     // parse origin
     try {
       settings.origin = new URL(opts.origin);
@@ -304,17 +451,7 @@ function XMLHttpRequest(opts) {
     }
 
     setState(this.OPENED);
-  };
-
-  /**
-   * Disables or enables isAllowedHttpHeader() check the request. Enabled by default.
-   * This does not conform to the W3C spec.
-   *
-   * @param boolean state Enable or disable header checking.
-   */
-  this.setDisableHeaderCheck = function(state) {
-    disableHeaderCheck = state;
-  };
+  });
 
   /**
    * Sets a header for the request.
@@ -323,8 +460,8 @@ function XMLHttpRequest(opts) {
    * @param string value Header value
    * @return boolean Header added
    */
-  this.setRequestHeader = function(header, value) {
-    if (this.readyState != this.OPENED) {
+  assignProp(this, 'setRequestHeader', function(header, value) {
+    if (readyState != this.OPENED) {
       throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN");
     }
     if (!isAllowedHttpHeader(header)) {
@@ -336,7 +473,24 @@ function XMLHttpRequest(opts) {
     }
     headers[header] = value;
     return true;
-  };
+  });
+
+  /**
+   * Gets a request header
+   *
+   * @deprecated
+   * @param string name Name of header to get
+   * @return string Returns the request header or empty string if not set
+   */
+  assignProp(this, 'getRequestHeader', function(name) {
+    // @TODO Make this case insensitive
+    console.warn("`xhr.getRequestHeader()` is deprecated and will be removed in a future release. It’s non-standard and not part of the XHR spec.");
+    if (typeof name === "string" && headers[name]) {
+      return headers[name];
+    }
+
+    return "";
+  });
 
   /**
    * Gets a header from the server response.
@@ -344,10 +498,10 @@ function XMLHttpRequest(opts) {
    * @param string header Name of header to get.
    * @return string Text of the header or null if it doesn't exist.
    */
-  this.getResponseHeader = function(header) {
+  assignProp(this, 'getResponseHeader', function(header) {
     // in case of local request, headers are not present
     if (typeof header === "string"
-      && this.readyState > this.OPENED
+      && readyState > this.OPENED
       && response.headers[header.toLowerCase()]
       && !errorFlag
       && response
@@ -357,16 +511,16 @@ function XMLHttpRequest(opts) {
     }
 
     return null;
-  };
+  });
 
   /**
    * Gets all the response headers.
    *
    * @return string A string with all response headers separated by CR+LF
    */
-  this.getAllResponseHeaders = function() {
+  assignProp(this, 'getAllResponseHeaders', function() {
     // in case of local request, headers are not present
-    if (this.readyState < this.HEADERS_RECEIVED || errorFlag || !response || !response.headers) {
+    if (readyState < this.HEADERS_RECEIVED || errorFlag || !response || !response.headers) {
       return "";
     }
     var result = "";
@@ -378,22 +532,7 @@ function XMLHttpRequest(opts) {
       }
     }
     return result.slice(0, -2);
-  };
-
-  /**
-   * Gets a request header
-   *
-   * @param string name Name of header to get
-   * @return string Returns the request header or empty string if not set
-   */
-  this.getRequestHeader = function(name) {
-    // @TODO Make this case insensitive
-    if (typeof name === "string" && headers[name]) {
-      return headers[name];
-    }
-
-    return "";
-  };
+  });
 
   /**
    * Convert from Data URI to Buffer
@@ -410,10 +549,21 @@ function XMLHttpRequest(opts) {
 
     if (parts.length < 2) throw "Invalid URL";
 
+    var dataHeaders = parts[0].split(";");
+
+    var base64 = false, charset;
+
     // check if header part has base64 (from 2nd header onwards)
-    var base64 = parts[0].split(";").some(function (dataHeader, index) {
-      return index > 0 && dataHeader.toLowerCase() === "base64";
-    });
+    // also get charset encoding of data URI (from FIRST found only)
+    for (var i = 1; i < dataHeaders.length; ++i) {
+      if (base64 && charset) break;
+      var header = dataHeaders[i];
+
+      if (!base64) base64 = header.toLowerCase() === "base64";
+      if (!charset && header.startsWith("charset=")) {
+        charset = header.slice(8).toLowerCase();
+      }
+    }
 
     var responseData, inputData = decodeURIComponent(parts[1]);
 
@@ -426,10 +576,16 @@ function XMLHttpRequest(opts) {
       inputData = inputData.slice(0, inputData.length - padding.length);
       responseData = Buffer.from(inputData, "base64");
       if (responseData.toString("base64").replace(/=+$/, "") !== inputData) throw "malformed base64 encoding";
-      return responseData;
+      return {
+        data: responseData,
+        charset: charset || "utf-8"
+      }
     }
     else {
-      return Buffer.from(inputData);
+      return {
+        data: Buffer.from(inputData),
+        charset: charset || "utf-8"
+      }
     }
   }
 
@@ -438,8 +594,8 @@ function XMLHttpRequest(opts) {
    *
    * @param string data Optional data to send as request body.
    */
-  this.send = function(data) {
-    if (this.readyState != this.OPENED) {
+  assignProp(this, 'send', function(data) {
+    if (readyState != this.OPENED) {
       throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");
     }
 
@@ -447,6 +603,10 @@ function XMLHttpRequest(opts) {
       throw new Error("INVALID_STATE_ERR: send has already been called");
     }
 
+    if (opts.syncPolicy === "disabled" && !settings.async) {
+      throw new Error("Synchronous requests are disabled for this instance.");
+    }
+
     var isSsl = false, isLocal = false, isDataUri = false;
     var url;
     try {
@@ -460,7 +620,7 @@ function XMLHttpRequest(opts) {
     }
     catch (e) {
       // URL parsing throws TypeError, here we only want to take its message
-      self.handleError(new Error(e.message));
+      handleError(new Error(e.message));
       return;
     }
     var host;
@@ -493,19 +653,20 @@ function XMLHttpRequest(opts) {
     // or data from Data URI (data:)
     if (isLocal) {
       if (isDataUri) try {
-        self.status = 200;
-        self.responseURL = settings.url;
-        self.createFileOrSyncResponse(bufferFromDataUri(url));
+        dataMap.status = 200;
+        dataMap.responseURL = settings.url;
+        var uriData = bufferFromDataUri(url);
+        createResponse(uriData.data, "text/plain; charset=" + uriData.charset);
         setState(self.DONE);
         return;
       }
       catch (e) {
-        self.handleError(new Error("Invalid data URI"));
+        handleError(new Error("Invalid data URI"));
         return;
       }
 
       if (!opts.allowFileSystemResources) {
-        self.handleError(new Error("Not allowed to load local resource: " + url.href));
+        handleError(new Error("Not allowed to load local resource: " + url.href));
         return;
       }
 
@@ -516,25 +677,25 @@ function XMLHttpRequest(opts) {
       if (settings.async) {
         fs.readFile(unescape(url.pathname), function(error, data) {
           if (error) {
-            self.handleError(error, error.errno || -1);
+            handleError(error, error.errno || -1);
           } else {
-            self.status = 200;
-            self.responseURL = settings.url;
+            dataMap.status = 200;
+            dataMap.responseURL = settings.url;
             // Use self.responseType to create the correct self.responseType, self.response.
-            self.createFileOrSyncResponse(data);
+            createResponse(data, "");
             setState(self.DONE);
           }
         });
       } else {
         try {
-          this.status = 200;
+          dataMap.status = 200;
           const syncData = fs.readFileSync(unescape(url.pathname));
           // Use self.responseType to create the correct self.responseType, self.response.
-          this.responseURL = settings.url;
-          this.createFileOrSyncResponse(syncData);
+          dataMap.responseURL = settings.url;
+          createResponse(syncData, "");
           setState(self.DONE);
         } catch(e) {
-          this.handleError(e, e.errno || -1);
+          handleError(e, e.errno || -1);
         }
       }
 
@@ -598,7 +759,7 @@ function XMLHttpRequest(opts) {
       sendFlag = true;
 
       // As per spec, this is called here for historical reasons.
-      self.dispatchEvent("readystatechange");
+      dispatchEvent("readystatechange");
 
       // Handler for the response
       var responseHandler = function(resp) {
@@ -614,7 +775,7 @@ function XMLHttpRequest(opts) {
           // end the response
           resp.destroy();
           if (redirectCount > maxRedirects) {
-            self.handleError(new Error("Too many redirects"));
+            handleError(new Error("Too many redirects"));
             return;
           }
           // Change URL to the redirect location
@@ -626,7 +787,7 @@ function XMLHttpRequest(opts) {
             settings.url = url.href;
           }
           catch (e) {
-            self.handleError(new Error("Unsafe redirect"));
+            handleError(new Error("Unsafe redirect"));
             return;
           }
           // change request options again to match with new redirect protocol
@@ -665,30 +826,12 @@ function XMLHttpRequest(opts) {
 
         setState(self.HEADERS_RECEIVED);
 
-        // When responseType is 'text' or '', self.responseText will be utf8 decoded text.
-        // When responseType is 'json', self.responseText initially will be utf8 decoded text,
-        // which is then JSON parsed into self.response.
-        // When responseType is 'arraybuffer', self.response is an ArrayBuffer.
-        // When responseType is 'blob', self.response is a Blob.
-        // cf. section 3.6, subsections 8,9,10,11 of https://xhr.spec.whatwg.org/#the-response-attribute
-        const isUtf8 = self.responseType === "" || self.responseType === "text" || self.responseType === "json";
-        if (isUtf8 && response.setEncoding) {
-          response.setEncoding("utf8");
-        }
-
-        self.status = response.statusCode;
+        dataMap.status = response.statusCode;
 
         response.on('data', function(chunk) {
           // Make sure there's some data
           if (chunk) {
-            if (isUtf8) {
-              // When responseType is 'text', '', 'json',
-              //   then each chunk is already utf8 decoded.
-              self.responseText += chunk;
-            } else {
-              // Otherwise collect the chunk buffers.
-              buffers.push(chunk);
-            }
+            buffers.push(chunk);
           }
           // Don't emit state changes if the connection has been aborted.
           if (sendFlag) {
@@ -698,20 +841,20 @@ function XMLHttpRequest(opts) {
 
         response.on('end', function() {
           if (sendFlag) {
-            // The sendFlag needs to be set before setState is called.  Otherwise if we are chaining callbacks
+            // The sendFlag needs to be set before setState is called. Otherwise if we are chaining callbacks
             // there can be a timing issue (the callback is called and a new call is made before the flag is reset).
             sendFlag = false;
             // Create the correct response for responseType.
-            self.createResponse(buffers);
-            self.statusText = this.statusMessage;
-            self.responseURL = settings.url;
+            createResponse(Buffer.concat(buffers), response.headers['content-type'] || "");
+            dataMap.statusText = this.statusMessage;
+            dataMap.responseURL = settings.url;
             // Discard the 'end' event if the connection has been aborted
             setState(self.DONE);
           }
         }.bind(response));
 
         response.on('error', function(error) {
-          self.handleError(error);
+          handleError(error);
         }.bind(response));
       }
 
@@ -721,11 +864,11 @@ function XMLHttpRequest(opts) {
         // don't fail the xhr request, attempt again.
         if (request.reusedSocket && error.code === 'ECONNRESET')
           return doRequest(options, responseHandler).on('error', errorHandler);
-        self.handleError(error);
+        handleError(error);
       }
 
       var createRequest = function (opt) {
-        opt = Object.assign({}, opt);
+        opt = Object.assign(Object.create(null), opt);
         if (isSsl) Object.assign(opt, sslOptions);
 
         request = doRequest(opt, responseHandler).on('error', errorHandler);
@@ -747,120 +890,129 @@ function XMLHttpRequest(opts) {
       // Create the request
       createRequest(options);
 
-      self.dispatchEvent("loadstart");
+      dispatchEvent("loadstart");
     } else { // Synchronous
-      // Create a temporary file for communication with the other Node process
-      var contentFile = ".node-xmlhttprequest-content-" + process.pid;
-      var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
-      fs.writeFileSync(syncFile, "", "utf8");
-      // The async request the other Node process executes
-      var execString = "'use strict';"
-        + "var http = require('http'), https = require('https'), fs = require('fs');"
-        + "function concat(bufferArray) {"
-        + "  let length = 0, offset = 0;"
-        + "  for (let k = 0; k < bufferArray.length; k++)"
-        + "    length += bufferArray[k].length;"
-        + "  const result = Buffer.alloc(length);"
-        + "  for (let k = 0; k < bufferArray.length; k++) {"
-        + "    for (let i = 0; i < bufferArray[k].length; i++) {"
-        + "      result[offset+i] = bufferArray[k][i]"
-        + "    }"
-        + "    offset += bufferArray[k].length;"
-        + "  }"
-        + "  return result;"
-        + "};"
-        + "var doRequest = http" + (isSsl ? "s" : "") + ".request;"
-        + "var isSsl = " + !!isSsl + ";"
-        + "var options = " + JSON.stringify(options) + ";"
-        + "var sslOptions = " + JSON.stringify(sslOptions) + ";"
-        + "var responseData = Buffer.alloc(0);"
-        + "var buffers = [];"
-        + "var url = new URL(" + JSON.stringify(settings.url) + ");"
-        + "var maxRedirects = " + maxRedirects + ", redirects_count = 0;"
-        + "var makeRequest = function () {"
-        + "  var opt = Object.assign({}, options);"
-        + "  if (isSsl) Object.assign(opt, sslOptions);"
-        + "  var req = doRequest(opt, function(response) {"
-        + "    if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307 || response.statusCode === 308) {"
-        + "      response.destroy();"
-        + "      ++redirects_count;"
-        + "      if (redirects_count > maxRedirects) {"
-        + "        fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Too many redirects', 'utf8');"
-        + "        fs.unlinkSync('" + syncFile + "');"
-        + "        return;"
-        + "      }"
-        + "      try {"
-        + "        url = new URL(response.headers.location, url);"
-        + "        if (url.protocol !== 'https:' && url.protocol !== 'http:') throw 'bad protocol';"
-        + "      }"
-        + "      catch (e) {"
-        + "        fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Unsafe redirect', 'utf8');"
-        + "        fs.unlinkSync('" + syncFile + "');"
-        + "        return;"
-        + "      };"
-        + "      isSsl = url.protocol === 'https:';"
-        + "      doRequest = isSsl ? https.request : http.request;"
-        + "      var port = url.port;"
-        + "      options = {"
-        + "        hostname: url.hostname,"
-        + "        port: port,"
-        + "        path: url.pathname + (url.search || ''),"
-        + "        method: response.statusCode === 303 ? 'GET' : options.method,"
-        + "        headers: options.headers"
-        + "      };"
-        + "      options.headers['Host'] = url.host;"
-        + "      if (!((isSsl && port === 443) || port === 80)) options.headers['Host'] += ':' + port;"
-        + "      makeRequest();"
-        + "      return;"
-        + "    }"
-        + "    response.on('data', function(chunk) {"
-        + "      buffers.push(chunk);"
-        + "    });"
-        + "    response.on('end', function() {"
-        + "      responseData = concat(buffers);"
-        + "      fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {url: url.href, statusCode: response.statusCode, statusText: response.statusMessage, headers: response.headers, data: responseData.toString('utf8')}}), 'utf8');"
-        + "      fs.unlinkSync('" + syncFile + "');"
-        + "    });"
-        + "    response.on('error', function(error) {"
-        + "      fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
-        + "      fs.unlinkSync('" + syncFile + "');"
-        + "    });"
-        + "  }).on('error', function(error) {"
-        + "    fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
-        + "    fs.unlinkSync('" + syncFile + "');"
-        + "  });"
-        + "  " + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"")
-        + "  req.end();"
-        + "};"
-        + "makeRequest();"
-      // Start the other Node Process, executing this string
-      var syncProc = spawn(process.argv[0], ["-e", execString]);
-      while(fs.existsSync(syncFile)) {
-        // Wait while the sync file is empty
+      try {
+        // Create a temporary file for communication with the other Node process
+        var tmpDir = os.tmpdir();
+        var syncResponse;
+        var contentFile = path.join(tmpDir, ".node-xmlhttprequest-content-" + process.pid);
+        var syncFile = path.join(tmpDir, ".node-xmlhttprequest-sync-" + process.pid);
+        fs.writeFileSync(syncFile, "", "utf8");
+        // The async request the other Node process executes
+        var execString = "'use strict';"
+          + "var http = require('http'), https = require('https'), fs = require('fs');"
+          + "function concat(bufferArray) {"
+          + "  let length = 0, offset = 0;"
+          + "  for (let k = 0; k < bufferArray.length; k++)"
+          + "    length += bufferArray[k].length;"
+          + "  const result = Buffer.alloc(length);"
+          + "  for (let k = 0; k < bufferArray.length; k++) {"
+          + "    for (let i = 0; i < bufferArray[k].length; i++) {"
+          + "      result[offset+i] = bufferArray[k][i]"
+          + "    }"
+          + "    offset += bufferArray[k].length;"
+          + "  }"
+          + "  return result;"
+          + "};"
+          + "var doRequest = http" + (isSsl ? "s" : "") + ".request;"
+          + "var isSsl = " + !!isSsl + ";"
+          + "var options = " + JSON.stringify(options) + ";"
+          + "var sslOptions = " + JSON.stringify(sslOptions) + ";"
+          + "var responseData = Buffer.alloc(0);"
+          + "var buffers = [];"
+          + "var url = new URL(" + JSON.stringify(settings.url) + ");"
+          + "var maxRedirects = " + maxRedirects + ", redirects_count = 0;"
+          + "var makeRequest = function () {"
+          + "  var opt = Object.assign(Object.create(null), options);"
+          + "  if (isSsl) Object.assign(opt, sslOptions);"
+          + "  var req = doRequest(opt, function(response) {"
+          + "    if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307 || response.statusCode === 308) {"
+          + "      response.destroy();"
+          + "      ++redirects_count;"
+          + "      if (redirects_count > maxRedirects) {"
+          + "        fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Too many redirects', 'utf8');"
+          + "        fs.unlinkSync('" + syncFile + "');"
+          + "        return;"
+          + "      }"
+          + "      try {"
+          + "        url = new URL(response.headers.location, url);"
+          + "        if (url.protocol !== 'https:' && url.protocol !== 'http:') throw 'bad protocol';"
+          + "      }"
+          + "      catch (e) {"
+          + "        fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR-REDIRECT: Unsafe redirect', 'utf8');"
+          + "        fs.unlinkSync('" + syncFile + "');"
+          + "        return;"
+          + "      };"
+          + "      isSsl = url.protocol === 'https:';"
+          + "      doRequest = isSsl ? https.request : http.request;"
+          + "      var port = url.port;"
+          + "      options = {"
+          + "        hostname: url.hostname,"
+          + "        port: port,"
+          + "        path: url.pathname + (url.search || ''),"
+          + "        method: response.statusCode === 303 ? 'GET' : options.method,"
+          + "        headers: options.headers"
+          + "      };"
+          + "      options.headers['Host'] = url.host;"
+          + "      if (!((isSsl && port === 443) || port === 80)) options.headers['Host'] += ':' + port;"
+          + "      makeRequest();"
+          + "      return;"
+          + "    }"
+          + "    response.on('data', function(chunk) {"
+          + "      buffers.push(chunk);"
+          + "    });"
+          + "    response.on('end', function() {"
+          + "      responseData = concat(buffers);"
+          + "      fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: { url: url.href, statusCode: response.statusCode, statusText: response.statusMessage, headers: response.headers }}), 'utf8');"
+          + "      fs.writeFileSync('" + contentFile + ".bin', responseData);"
+          + "      fs.unlinkSync('" + syncFile + "');"
+          + "    });"
+          + "    response.on('error', function(error) {"
+          + "      fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
+          + "      fs.unlinkSync('" + syncFile + "');"
+          + "    });"
+          + "  }).on('error', function(error) {"
+          + "    fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');"
+          + "    fs.unlinkSync('" + syncFile + "');"
+          + "  });"
+          + "  " + (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"")
+          + "  req.end();"
+          + "};"
+          + "makeRequest();"
+        // Start the other Node Process, executing this string
+        var syncProc = spawn(process.argv[0], ["-e", execString]);
+        while(fs.existsSync(syncFile)) {
+          // Wait while the sync file is empty
+        }
+        syncResponse = fs.readFileSync(contentFile, 'utf8');
+        // Kill the child process once the file has data
+        syncProc.stdin.end();
+        // Remove the temporary file
+        fs.unlinkSync(contentFile);
+      }
+      catch (e) {
+        handleError(new Error("Synchronous operation aborted: Unable to access the OS temporary directory for read/write operations."));
       }
-      self.responseText = fs.readFileSync(contentFile, 'utf8');
-      // Kill the child process once the file has data
-      syncProc.stdin.end();
-      // Remove the temporary file
-      fs.unlinkSync(contentFile);
-      if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR(-REDIRECT){0,1}:/)) {
+      if (syncResponse.match(/^NODE-XMLHTTPREQUEST-ERROR(-REDIRECT){0,1}:/)) {
         // If the file returned an error, handle it
-        if (self.responseText.startsWith('NODE-XMLHTTPREQUEST-ERROR-REDIRECT')) {
-          self.handleError(new Error(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR-REDIRECT: /, "")));
+        if (syncResponse.startsWith('NODE-XMLHTTPREQUEST-ERROR-REDIRECT')) {
+          handleError(new Error(syncResponse.replace(/^NODE-XMLHTTPREQUEST-ERROR-REDIRECT: /, "")));
         }
         else {
-          var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
-          self.handleError(errorObj, 503);
+          var errorObj = JSON.parse(syncResponse.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
+          handleError(errorObj, 503);
         }
-      } else {
+      } else try {
         // If the file returned okay, parse its data and move to the DONE state
-        const resp = JSON.parse(self.responseText);
-        self.status = resp.data.statusCode;
-        self.statusText = resp.data.statusText;
-        self.responseURL = resp.data.url;
-        self.response = stringToBuffer(resp.data.data);
+        const resp = JSON.parse(syncResponse);
+        dataMap.status = resp.data.statusCode;
+        dataMap.statusText = resp.data.statusText;
+        dataMap.responseURL = resp.data.url;
+        dataMap.response = fs.readFileSync(contentFile + ".bin");
+        fs.unlinkSync(contentFile + ".bin");
         // Use self.responseType to create the correct self.responseType, self.response, self.responseXML.
-        self.createFileOrSyncResponse(self.response);
+        createResponse(self.response, resp.data.headers["content-type"] || "");
         // Set up response correctly.
         response = {
           statusCode: self.status,
@@ -868,87 +1020,79 @@ function XMLHttpRequest(opts) {
         };
         setState(self.DONE);
       }
+      catch (e) {
+        handleError(new Error("Synchronous operation aborted: Unable to access the OS temporary directory for read/write operations."));
+      }
     }
-  };
-
-  /**
-   * Called when an error is encountered to deal with it.
-   * @param  status  {number}    HTTP status code to use rather than the default (0) for XHR errors.
-   */
-  this.handleError = function(error, status) {
-    this.status = status || 0;
-    this.statusText = error.message || "";
-    this.responseText = "";
-    this.responseXML = "";
-    this.responseURL = "";
-    this.response = Buffer.alloc(0);
-    errorFlag = true;
-    setState(this.DONE);
-    if (!settings.async) throw error;
-  };
+  });
 
   /**
    * Aborts a request.
    */
-  this.abort = function() {
+  var abort = function() {
     if (request) {
       request.abort();
       request = null;
     }
 
-    headers = Object.assign({}, defaultHeaders);
-    this.responseText = "";
-    this.responseXML = "";
-    this.response = Buffer.alloc(0);
+    headers = Object.assign(Object.create(null), defaultHeaders);
+    dataMap.responseText = "";
+    dataMap.responseXML = "";
+    dataMap.response = Buffer.alloc(0);
 
     errorFlag = abortedFlag = true
-    if (this.readyState !== this.UNSENT
-        && (this.readyState !== this.OPENED || sendFlag)
-        && this.readyState !== this.DONE) {
+    if (readyState !== self.UNSENT
+        && (readyState !== self.OPENED || sendFlag)
+        && readyState !== self.DONE) {
       sendFlag = false;
-      setState(this.DONE);
+      setState(self.DONE);
     }
-    this.readyState = this.UNSENT;
+    readyState = self.UNSENT;
   };
 
+  /**
+   * Aborts a request.
+   */
+  assignProp(this, 'abort', abort);
+
   /**
    * Adds an event listener. Preferred method of binding to events.
    */
-  this.addEventListener = function(event, callback) {
+  assignProp(this, 'addEventListener', function(event, callback) {
     if (!(event in listeners)) {
       listeners[event] = [];
     }
     // Currently allows duplicate callbacks. Should it?
     listeners[event].push(callback);
-  };
+  });
 
   /**
    * Remove an event callback that has already been bound.
    * Only works on the matching funciton, cannot be a copy.
    */
-  this.removeEventListener = function(event, callback) {
+  assignProp(this, 'removeEventListener', function(event, callback) {
     if (event in listeners) {
       // Filter will return a new array with the callback removed
       listeners[event] = listeners[event].filter(function(ev) {
         return ev !== callback;
       });
     }
-  };
+  });
 
   /**
    * Dispatch any events, including both "on" methods and events attached using addEventListener.
    */
-  this.dispatchEvent = function (event) {
+  var dispatchEvent = function (event) {
     let argument = { type: event };
     if (typeof self["on" + event] === "function") {
-      if (this.readyState === this.DONE && settings.async)
+      if (readyState === self.DONE && settings.async)
         setTimeout(function() { self["on" + event](argument) }, 0)
       else
         self["on" + event](argument)
     }
     if (event in listeners) {
       for (let i = 0, len = listeners[event].length; i < len; i++) {
-        if (this.readyState === this.DONE)
+        if (readyState === self.DONE)
           setTimeout(function() { listeners[event][i].call(self, argument) }, 0)
         else
           listeners[event][i].call(self, argument)
@@ -957,74 +1101,9 @@ function XMLHttpRequest(opts) {
   };
 
   /**
-   * Construct the correct form of response, given responseType when in non-file based, asynchronous mode.
-   *
-   * When self.responseType is "", "text", "json", self.responseText is a utf8 string.
-   * When self.responseType is "arraybuffer", "blob", the response is in the buffers parameter,
-   * an Array of Buffers. Then concat(buffers) is Uint8Array, from which checkAndShrinkBuffer
-   * extracts the correct sized ArrayBuffer.
-   *
-   * @param {Array<Buffer>} buffers
-   */
-  this.createResponse = function(buffers) {
-    self.responseXML = '';
-    switch (self.responseType) {
-      case "":
-      case "text":
-        self.response = self.responseText;
-        break;
-      case 'json':
-        self.response = JSON.parse(self.responseText);
-        self.responseText = '';
-        break;
-      default:
-        self.responseText = '';
-        const totalResponse = concat(buffers);
-        // When self.responseType === 'arraybuffer', self.response is an ArrayBuffer.
-        // Get the correct sized ArrayBuffer.
-        self.response = checkAndShrinkBuffer(totalResponse);
-        if (self.responseType === 'blob' && typeof Blob === 'function') {
-          // Construct the Blob object that contains response.
-          self.response = new Blob([self.response]);
-        }
-        break;
-    }
-  }
-
-  /**
-   * Construct the correct form of response, given responseType when in synchronous mode or file based.
-   *
-   * The input is the response parameter which is a Buffer.
-   * When self.responseType is "", "text", "json",
-   *   the input is further refined to be: response.toString('utf8').
-   * When self.responseType is "arraybuffer", "blob",
-   *   the input is further refined to be: checkAndShrinkBuffer(response).
-   *
-   * @param {Buffer} response
+   * Dispatch any events, including both "on" methods and events attached using addEventListener.
    */
-  this.createFileOrSyncResponse = function(response) {
-    self.responseText = '';
-    self.responseXML = '';
-    switch (self.responseType) {
-      case "":
-      case "text":
-        self.responseText = response.toString('utf8');
-        self.response = self.responseText;
-        break;
-      case 'json':
-        self.response = JSON.parse(response.toString('utf8'));
-        break;
-      default:
-        // When self.responseType === 'arraybuffer', self.response is an ArrayBuffer.
-        // Get the correct sized ArrayBuffer.
-        self.response = checkAndShrinkBuffer(response);
-        if (self.responseType === 'blob' && typeof Blob === 'function') {
-          // Construct the Blob object that contains response.
-          self.response = new Blob([self.response]);
-        }
-        break;
-    }
-  }
+  assignProp(this, 'dispatchEvent', dispatchEvent);
 
   /**
    * Changes readyState and calls onreadystatechange.
@@ -1032,16 +1111,16 @@ function XMLHttpRequest(opts) {
    * @param int state New state
    */
   var setState = function(state) {
-    if ((self.readyState === state) || (self.readyState === self.UNSENT && abortedFlag))
+    if ((readyState === state) || (readyState === self.UNSENT && abortedFlag))
       return
 
-    self.readyState = state;
+    readyState = state;
 
-    if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) {
-      self.dispatchEvent("readystatechange");
+    if (settings.async || readyState < self.OPENED || readyState === self.DONE) {
+      dispatchEvent("readystatechange");
     }
 
-    if (self.readyState === self.DONE) {
+    if (readyState === self.DONE) {
       let fire
 
       if (abortedFlag)
@@ -1051,10 +1130,10 @@ function XMLHttpRequest(opts) {
       else
         fire = "load"
 
-      self.dispatchEvent(fire)
+      dispatchEvent(fire)
 
       // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie)
-      self.dispatchEvent("loadend");
+      dispatchEvent("loadend");
     }
   };
 };
diff --git a/package.json b/package.json
index 8db9dfb..926be7f 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
   },
   "bugs": "http://github.com/mjwwit/node-XMLHttpRequest/issues",
   "engines": {
-    "node": ">=12.0.0"
+    "node": ">=13.0.0"
   },
   "scripts": {
     "test": "cd ./tests && node run-test.js"
diff --git a/tests/server.js b/tests/server.js
index e76752b..28fe380 100644
--- a/tests/server.js
+++ b/tests/server.js
@@ -1,6 +1,26 @@
 'use strict';
 var http = require("http");
 
+var bufferBody = Buffer.from([
+    0x48, // H
+    0xE9, // é
+    0x6C, // l
+    0x6C, // l
+    0x6F, // o
+    0x20, //  
+    0x77, // w
+    0xF8, // ø
+    0x72, // r
+    0x6C, // l
+    0x64, // d
+    0x20, //  
+    0x6E, // n
+    0x61, // a
+    0xEF, // ï
+    0x76, // v
+    0x65  // e
+]);
+
 var server = http.createServer(function (req, res) {
     switch (req.url) {
         case "/": {
@@ -32,10 +52,23 @@ var server = http.createServer(function (req, res) {
             return;
         case "/binary2":
             const ta = new Float32Array([1, 5, 6, 7]);
-            const buf = Buffer.from(ta.buffer);
-            const str = buf.toString('binary');
+            const buf = Buffer.from(ta);
             res.writeHead(200, {"Content-Type": "application/octet-stream"})
-            res.end(str);
+            res.end(buf);
+            return;
+        case "/latin1":
+            res.writeHead(200, {
+                'Content-Type': 'text/plain; charset=ISO-8859-1',
+                'Content-Length': bufferBody.length
+            });
+            res.end(bufferBody);
+            return;
+        case "/latin1-invalid":
+            res.writeHead(200, {
+                'Content-Type': 'text/plain; charset=lorem_ipsum',
+                'Content-Length': bufferBody.length
+            });
+            res.end(bufferBody);
             return;
         default:
             if (req.url.startsWith('/redirectingResource/')) {
diff --git a/tests/test-constants.js b/tests/test-constants.js
index 57e1780..0cd7ec4 100644
--- a/tests/test-constants.js
+++ b/tests/test-constants.js
@@ -3,6 +3,12 @@ var assert = require("assert")
   , xhr = new XMLHttpRequest();
 
 // Test constant values
+assert.equal(0, XMLHttpRequest.UNSENT);
+assert.equal(1, XMLHttpRequest.OPENED);
+assert.equal(2, XMLHttpRequest.HEADERS_RECEIVED);
+assert.equal(3, XMLHttpRequest.LOADING);
+assert.equal(4, XMLHttpRequest.DONE);
+
 assert.equal(0, xhr.UNSENT);
 assert.equal(1, xhr.OPENED);
 assert.equal(2, xhr.HEADERS_RECEIVED);
diff --git a/tests/test-data-uri.js b/tests/test-data-uri.js
index c94dd23..e7c3314 100644
--- a/tests/test-data-uri.js
+++ b/tests/test-data-uri.js
@@ -1,8 +1,5 @@
 var assert = require("assert")
-  , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest
-  , xhr;
-
-xhr = new XMLHttpRequest();
+  , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest;
 
 // define test data
 var tests = [
@@ -63,7 +60,7 @@ var tests_passed = 0;
 var runAsyncTest = function (test) {
   console.log("  ASYNC");
 
-  xhr = new XMLHttpRequest;
+  var xhr = new XMLHttpRequest;
   xhr.open("get", test.data);
   xhr.onreadystatechange = function () {
     if (this.readyState === 4) {
@@ -75,7 +72,7 @@ var runAsyncTest = function (test) {
         assert.equal(xhr.status, 200);
         assert.equal(xhr.responseText, test.output);
       }
-      console.log("    --> SUCESS");
+      console.log("    --> SUCCESS");
       ++tests_passed;
     }
   }
@@ -85,7 +82,7 @@ var runAsyncTest = function (test) {
 var runSyncTest = function (test) {
   console.log("  SYNC");
 
-  xhr = new XMLHttpRequest;
+  var xhr = new XMLHttpRequest;
   xhr.open("get", test.data, false);
   try {
     xhr.send();
@@ -108,7 +105,13 @@ var startTest = function () {
   let test = tests[i];
 
   if (!test) {
-    console.log("Done:", tests_passed === tests.length * 2 ? "PASS" : "FAILED");
+    console.log(tests_passed, "/", tests.length * 2, "tests passed");
+    if (tests_passed === tests.length * 2)
+      console.log("Done: PASS");
+    else {
+      console.error("Done: FAILED");
+      throw "";
+    }
     return;
   }
 
diff --git a/tests/test-headers.js b/tests/test-headers.js
index e22e5f9..c957103 100644
--- a/tests/test-headers.js
+++ b/tests/test-headers.js
@@ -5,14 +5,23 @@ var assert = require("assert")
 
 // Test server
 var server = http.createServer(function (req, res) {
-  // Test setRequestHeader
-  assert.equal("Foobar", req.headers["x-test"]);
-  // Test non-conforming allowed header
-  assert.equal("node-XMLHttpRequest-test", req.headers["user-agent"]);
-  // Test header set with blacklist disabled
-  assert.equal("http://github.com", req.headers["referer"]);
-  // Test case insensitive header was set
-  assert.equal("text/plain", req.headers["content-type"]);
+  switch (req.url) {
+    case "/allow":
+      // Test disabling header check
+      assert.equal("http://github.com", req.headers["referer"]);
+      console.log("No header check: PASSED");
+      break;
+    default:
+      // Test setRequestHeader
+      assert.equal("Foobar", req.headers["x-test"]);
+      // Test non-conforming allowed header
+      assert.equal("node-XMLHttpRequest-test", req.headers["user-agent"]);
+      // Test case insensitive header was set
+      assert.equal("text/plain", req.headers["content-type"]);
+      // Test forbidden header
+      assert.equal(null, req.headers["referer"]);
+      console.log("Strict header check: PASSED");
+  }
 
   var body = "Hello World";
   res.writeHead(200, {
@@ -48,7 +57,7 @@ xhr.onreadystatechange = function() {
     assert.equal("", this.getAllResponseHeaders());
     assert.equal(null, this.getResponseHeader("Connection"));
 
-    console.log("done");
+    console.log("Response headers check: PASSED");
   }
 };
 
@@ -58,24 +67,28 @@ try {
   var body = "Hello World";
   // Valid header
   xhr.setRequestHeader("X-Test", "Foobar");
-  // Invalid header
+  // Invalid header Content-Length
   xhr.setRequestHeader("Content-Length", Buffer.byteLength(body));
+  // Invalid header Referer
+  xhr.setRequestHeader("Referer", "http://github.com");
   // Allowed header outside of specs
   xhr.setRequestHeader("user-agent", "node-XMLHttpRequest-test");
   // Case insensitive header
   xhr.setRequestHeader("content-type", 'text/plain');
-  // Test getRequestHeader
-  assert.equal("Foobar", xhr.getRequestHeader("X-Test"));
-  // Test invalid header
-  assert.equal("", xhr.getRequestHeader("Content-Length"));
+  xhr.send(body);
+} catch(e) {
+  console.error("ERROR: Exception raised", e);
+  throw e;
+}
 
+try {
   // Test allowing all headers
-  xhr.setDisableHeaderCheck(true);
+  xhr = new XMLHttpRequest({ disableHeaderCheck: true });
+  xhr.open("POST", "http://localhost:8000/allow");
   xhr.setRequestHeader("Referer", "http://github.com");
-  assert.equal("http://github.com", xhr.getRequestHeader("Referer"));
-
-  xhr.send(body);
-} catch(e) {
+  xhr.send();
+}
+catch (e) {
   console.error("ERROR: Exception raised", e);
   throw e;
 }
diff --git a/tests/test-keepalive.js b/tests/test-keepalive.js
index a5b09e7..f7d22bd 100644
--- a/tests/test-keepalive.js
+++ b/tests/test-keepalive.js
@@ -34,4 +34,4 @@ var interval = setInterval(function sendRequest() {
     }
   }
   xhr.send();
-}, 200);
\ No newline at end of file
+}, 200);
diff --git a/tests/test-max-redirects.js b/tests/test-max-redirects.js
index 9938528..1afd3ec 100644
--- a/tests/test-max-redirects.js
+++ b/tests/test-max-redirects.js
@@ -11,7 +11,7 @@ var runTest = function () {
     xhr.open("GET", "http://localhost:8888/redirectingResource/10", false);
     xhr.onreadystatechange = function() {
     if (xhr.readyState === 4) {
-      assert.equal(xhr.getRequestHeader('Location'), '');
+      // assert.equal(xhr.getRequestHeader('Location'), '');
       assert.equal(xhr.responseText, "Hello World");
       console.log("safe redirects count: done");
     }
diff --git a/tests/test-mimetype.js b/tests/test-mimetype.js
new file mode 100644
index 0000000..5e65d21
--- /dev/null
+++ b/tests/test-mimetype.js
@@ -0,0 +1,168 @@
+var assert = require("assert")
+  , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest
+  , spawn = require('child_process').spawn
+  , serverProcess;
+
+const body = Buffer.from([
+  0x48, // H
+  0xE9, // é
+  0x6C, // l
+  0x6C, // l
+  0x6F, // o
+  0x20, //  
+  0x77, // w
+  0xF8, // ø
+  0x72, // r
+  0x6C, // l
+  0x64, // d
+  0x20, //  
+  0x6E, // n
+  0x61, // a
+  0xEF, // ï
+  0x76, // v
+  0x65  // e
+]);
+
+var base64Str = function (charset) {
+  return "data:text/plain;base64;charset=" + charset + "," + body.toString('base64');
+}
+
+// specify custom decoder to work on older node versions
+var decodeTextFromBuffer = function (buf, enc) {
+  if (enc == "iso-8859-1") return buf.toString("latin1");
+  return new TextDecoder(enc).decode(buf);
+}
+
+var plainStr = decodeTextFromBuffer(body, "iso-8859-1");
+var plainStrUTF8 = decodeTextFromBuffer(body, "utf-8");
+
+var createXHRInstance = function (opts) {
+  return new XMLHttpRequest(Object.assign({ textDecoder: null }, opts));
+}
+
+// spawn a server
+serverProcess = spawn(process.argv[0], [__dirname + "/server.js"], { stdio: 'inherit' });
+
+setTimeout(function () {
+  try {
+    runTest();
+    console.log('PASSED');
+  } catch (e) {
+    console.log('FAILED');
+    serverProcess.kill('SIGINT');
+    throw e;
+  } finally {
+    
+  }
+}, 100);
+
+var tests = [
+  {
+    name: "XHR with default latin-1 encoding",
+    endpoint: "http://localhost:8888/latin1",
+    expected: plainStr
+  },
+  {
+    name: "XHR with overrideMimeType charset=latin-1",
+    endpoint: "http://localhost:8888/latin1-invalid",
+    override: "text/plain; charset=ISO-8859-1",
+    expected: plainStr
+  },
+  {
+    name: "XHR with wrong charset (utf-8 expected, actual is latin-1)",
+    endpoint: "http://localhost:8888/latin1-invalid",
+    expected: ''
+  },
+  {
+    name: "XHR with wrong overriden charset (utf-8 expected, actual is latin-1)",
+    endpoint: "http://localhost:8888/latin1-invalid",
+    override: "text/plain; charset=lorem_ipsum",
+    expected: plainStrUTF8
+  },
+  {
+    name: "XHR on data URI with Latin-1 and charset specified",
+    endpoint: base64Str("ISO-8859-1"),
+    expected: plainStr
+  },
+  {
+    name: "XHR on data URI with overrideMimeType to Latin-1",
+    endpoint: base64Str("UTF-8"),
+    override: "text/plain; charset=ISO-8859-1",
+    expected: plainStr
+  },
+  {
+    name: "XHR on data URI with wrong default charset (utf-8 vs latin-1)",
+    endpoint: base64Str("lorem_ipsum"),
+    expected: ''
+  },
+  {
+    name: "XHR with wrong overriden charset and Data URI (utf-8 expected, actual is latin-1)",
+    endpoint: base64Str("iso-8859-1"),
+    override: "text/plain; charset=lorem_ipsum",
+    expected: plainStrUTF8
+  }
+];
+
+var tests_passed = 0;
+
+var total_tests = tests.length * 2;
+
+var runSyncTest = function (i) {
+  var test = tests[i];
+  var index = i + 1;
+  try {
+    var xhr = createXHRInstance();
+    console.log("Test " + index + ": [SYNC] " + test.name);
+    xhr.open("GET", test.endpoint, false);
+    if (test.override) xhr.overrideMimeType(test.override);
+    xhr.send();
+    assert.equal(xhr.responseText, test.expected);
+    console.log("Test " + index + ": PASSED");
+    ++tests_passed;
+  } catch (e) {
+    console.log("Test " + index + ": FAILED with exception", e);
+  }
+}
+
+var runAsyncTest = function (i) {
+  if (i >= tests.length) {
+    serverProcess.kill('SIGINT');
+    if (tests_passed === total_tests) return console.log("ALL PASSED");
+    else {
+      console.error("FAILED: Only " + tests_passed + " / " + total_tests + " tests passed");
+      throw "";
+    };
+  }
+  var test = tests[i];
+  var index = i + tests.length + 1;
+  try {
+    var xhr = createXHRInstance();
+    console.log("Test " + index + ": [ASYNC] " + test.name);
+    xhr.open("GET", test.endpoint);
+    if (test.override) xhr.overrideMimeType(test.override);
+    xhr.onreadystatechange = function () {
+      if (xhr.readyState === 4) try {
+        assert.equal(xhr.responseText, test.expected);
+        console.log("Test " + index + ": PASSED");
+        ++tests_passed;
+        runAsyncTest(i + 1);
+      }
+      catch (e) {
+        console.log("Test " + index + ": FAILED with exception", e);
+        runAsyncTest(i + 1);
+      }
+    }
+    xhr.send();
+  } catch (e) {
+    console.log("Test " + index + ": FAILED with exception", e);
+    runAsyncTest(i + 1);
+  }
+}
+
+var runTest = function () {
+  for (var i = 0; i < tests.length; i++) {
+    runSyncTest(i);
+  }
+
+  runAsyncTest(0);
+}
diff --git a/tests/test-pollution.js b/tests/test-pollution.js
new file mode 100644
index 0000000..9fbc516
--- /dev/null
+++ b/tests/test-pollution.js
@@ -0,0 +1,61 @@
+// Main purpose of this test is to ensure that XHR options and exposed methods cannot be prototype-polluted
+
+var XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest,
+  spawn = require("child_process").spawn,
+  assert = require("assert"),
+  xhr,
+  objectProto = Object.getPrototypeOf({});
+
+// spawn a server
+serverProcess = spawn(process.argv[0], [__dirname + "/server.js"], { stdio: 'inherit' });
+
+var polluteFunc = function (buf, enc) {
+  return "Polluted!";
+}
+
+var runTest = function () {
+  // most naive pollution
+  objectProto.textDecoder = polluteFunc;
+
+  xhr = new XMLHttpRequest();
+  xhr.open("GET", "http://localhost:8888", false);
+  xhr.send();
+  assert.equal("Hello World", xhr.responseText);
+  console.log("Naive pollution: PASSED");
+
+  delete objectProto.textDecoder;
+
+  // pollute with getter/setter
+  Object.defineProperty(objectProto, 'textDecoder', {
+    get: function () { return polluteFunc; },
+    set: function (value) {}
+  });
+
+  xhr = new XMLHttpRequest();
+  xhr.open("GET", "http://localhost:8888", false);
+  xhr.send();
+  assert.equal("Hello World", xhr.responseText);
+  console.log("Getter/Setter pollution: PASSED");
+
+  // pollute xhr properties
+  Object.defineProperty(objectProto, 'responseText', {
+    get: function () { return "Polluted!"; },
+    set: function (value) {}
+  });
+
+  xhr = new XMLHttpRequest();
+  xhr.open("GET", "http://localhost:8888", false);
+  xhr.send();
+  assert.equal("Hello World", xhr.responseText);
+  console.log("Pollute xhr.responseText: PASSED");
+}
+
+try {
+  runTest();
+}
+catch (e) {
+  throw e;
+}
+finally {
+  serverProcess.kill('SIGINT');
+}
diff --git a/tests/test-redirect-301.js b/tests/test-redirect-301.js
index 91ec4cf..c116533 100644
--- a/tests/test-redirect-301.js
+++ b/tests/test-redirect-301.js
@@ -26,7 +26,7 @@ var server = http.createServer(function (req, res) {
 
 xhr.onreadystatechange = function() {
   if (this.readyState === 4) {
-    assert.equal(xhr.getRequestHeader('Location'), '');
+    // assert.equal(xhr.getRequestHeader('Location'), '');
     assert.equal(xhr.responseText, "Hello World");
     console.log("done");
   }
diff --git a/tests/test-redirect-302.js b/tests/test-redirect-302.js
index 802e948..ef32808 100644
--- a/tests/test-redirect-302.js
+++ b/tests/test-redirect-302.js
@@ -26,7 +26,7 @@ var server = http.createServer(function (req, res) {
 
 xhr.onreadystatechange = function() {
   if (this.readyState === 4) {
-    assert.equal(xhr.getRequestHeader('Location'), '');
+    // assert.equal(xhr.getRequestHeader('Location'), '');
     assert.equal(xhr.responseText, "Hello World");
     console.log("done");
   }
diff --git a/tests/test-redirect-303.js b/tests/test-redirect-303.js
index 4d51962..717aa42 100644
--- a/tests/test-redirect-303.js
+++ b/tests/test-redirect-303.js
@@ -26,7 +26,7 @@ var server = http.createServer(function (req, res) {
 
 xhr.onreadystatechange = function() {
   if (this.readyState === 4) {
-    assert.equal(xhr.getRequestHeader('Location'), '');
+    // assert.equal(xhr.getRequestHeader('Location'), '');
     assert.equal(xhr.responseText, "Hello World");
     console.log("done");
   }
diff --git a/tests/test-redirect-307.js b/tests/test-redirect-307.js
index 6e8cb9f..04ff971 100644
--- a/tests/test-redirect-307.js
+++ b/tests/test-redirect-307.js
@@ -28,7 +28,7 @@ var server = http.createServer(function (req, res) {
 
 xhr.onreadystatechange = function() {
   if (this.readyState === 4) {
-    assert.equal(xhr.getRequestHeader('Location'), '');
+    // assert.equal(xhr.getRequestHeader('Location'), '');
     assert.equal(xhr.responseText, "Hello World");
     console.log("done");
   }
diff --git a/tests/test-redirect-308.js b/tests/test-redirect-308.js
index d517f68..6de9f7b 100644
--- a/tests/test-redirect-308.js
+++ b/tests/test-redirect-308.js
@@ -26,7 +26,7 @@ var server = http.createServer(function (req, res) {
 
 xhr.onreadystatechange = function() {
   if (this.readyState === 4) {
-    assert.equal(xhr.getRequestHeader('Location'), '');
+    // assert.equal(xhr.getRequestHeader('Location'), '');
     assert.equal(xhr.responseText, "Hello World");
     console.log("done");
   }
diff --git a/tests/test-sync-flag.js b/tests/test-sync-flag.js
new file mode 100644
index 0000000..8f9af5c
--- /dev/null
+++ b/tests/test-sync-flag.js
@@ -0,0 +1,107 @@
+var assert = require("assert")
+  , XMLHttpRequest = require("../lib/XMLHttpRequest").XMLHttpRequest
+  , spawn = require("child_process").spawn
+  , serverProcess
+  , process = require("process");
+
+// spawn a server
+serverProcess = spawn(process.argv[0], [__dirname + "/server.js"], { stdio: 'inherit' });
+
+setTimeout(function () {
+  try {
+    runTest();
+    console.log('PASSED');
+  } catch (e) {
+    console.log('FAILED');
+    throw e;
+  } finally {
+    serverProcess.kill('SIGINT');
+  }
+}, 100);
+
+/**
+ * stage = 0 // idle
+ * stage = 1 // expect warning to check
+ * stage = 2 // available but does not expect warning
+ */
+var stage = 0;
+// warning catch
+let oldWarn = console.warn;
+console.warn = function (warning) {
+  if (stage > 0) {
+    if (stage === 1) {
+      assert.equal(warning, "[Deprecation] Synchronous XMLHttpRequest is deprecated because of its detrimental effects to the end user's experience. For more information, see https://xhr.spec.whatwg.org/#sync-flag");
+      console.log("Correct warning caught.");
+    }
+    else if (stage === 2) {
+      throw "Does not expect warning, caught " + JSON.stringify(warning);
+    }
+  }
+
+  return oldWarn.call(this, warning);
+}
+
+var runTest = function () {
+  // xhr with no syncPolicy (default = warn)
+  try {
+    console.log("Testing 1: XHR with no syncPolicy (default = warn)");
+    var xhr = new XMLHttpRequest();
+    stage = 1;
+    xhr.open("GET", "http://localhost:8888/text", false);
+    stage = 0;
+    xhr.send();
+    assert.equal(xhr.responseText, "Hello world!");
+    console.log("Test 1: PASSED");
+  } catch(e) {
+    console.log("ERROR: Exception raised", e);
+    throw e;
+  }
+
+  // xhr with syncPolicy = warn
+  try {
+    console.log("Testing 2: XHR with syncPolicy = warn");
+    var xhr = new XMLHttpRequest({ syncPolicy: "warn" });
+    stage = 1;
+    xhr.open("GET", "http://localhost:8888/text", false);
+    stage = 0;
+    xhr.send();
+    assert.equal(xhr.responseText, "Hello world!");
+    console.log("Test 2: PASSED");
+  } catch(e) {
+    console.log("ERROR: Exception raised", e);
+    throw e;
+  }
+
+  // xhr with syncPolicy = enabled
+  try {
+    console.log("Testing 3: XHR with syncPolicy = enabled");
+    var xhr = new XMLHttpRequest({ syncPolicy: "enabled" });
+    stage = 2;
+    xhr.open("GET", "http://localhost:8888/text", false);
+    stage = 0;
+    xhr.send();
+    assert.equal(xhr.responseText, "Hello world!");
+    console.log("Test 3: PASSED");
+  } catch(e) {
+    console.log("ERROR: Exception raised", e);
+    throw e;
+  }
+
+  // xhr with syncPolicy = disabled
+  var errored = false;
+  try {
+    console.log("Testing 4: XHR with syncPolicy = disabled");
+    var xhr = new XMLHttpRequest({ syncPolicy: "disabled" });
+    stage = 2;
+    xhr.open("GET", "http://localhost:8888/text", false);
+    stage = 0;
+    xhr.send();
+  } catch(e) {
+    errored = true;
+    assert.equal(e.message, "Synchronous requests are disabled for this instance.");
+    console.log("Correct error message.")
+    console.log("Test 4: PASSED");
+  }
+
+  if (!errored) throw "Test 4 expects an error.";
+}
diff --git a/tests/test-sync-response.js b/tests/test-sync-response.js
index 734fe01..a9ae473 100644
--- a/tests/test-sync-response.js
+++ b/tests/test-sync-response.js
@@ -30,21 +30,6 @@ setTimeout(function () {
   }
 }, 100);
 
-/**
- * Assumes hexStr is the in-memory representation of a Float32Array.
- * Relies on the fact that the char codes in hexStr are all <= 0xFF.
- * Returns Float32Array corresponding to hexStr.
- *
- * @param {string} hexStr
- * @returns {Float32Array}
- */
-function stringToFloat32Array (hexStr) {
-  const u8 = new Uint8Array(hexStr.length);
-  for (let k = 0; k < hexStr.length; k++)
-    u8[k] = Number(hexStr.charCodeAt(k));
-  return new Float32Array(u8.buffer);
-}
-
 /**
  * Check to see if 2 array-like objects have the same elements.
  * @param {{ length: number }} ar1
@@ -106,8 +91,8 @@ function runTest() {
   xhr.onreadystatechange = function () {
     if (xhr.readyState === 4) {
       // xhr.response is an ArrayBuffer
-      var binaryStr = Buffer.from(xhr.response).toString('binary');
-      var f32 = stringToFloat32Array(binaryStr);
+      var binary = Buffer.from(xhr.response);
+      var f32 = new Float32Array(binary);
       log('/binary2', f32);
       var answer = new Float32Array([1, 5, 6, 7]);
       assert.equal(isEqual(f32, answer), true);


Reviews:
user: login: mjwwit
body: Looking good! The `syncPolicy` `"disabled"` behavior contains a bug though; when sending an async request it still throws an error. Maybe this would also be a good candidate for a test.
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-13T06:23:42Z
user: login: Bhpsngum
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-13T06:30:24Z
user: login: Bhpsngum
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-13T06:31:48Z
user: login: mjwwit
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T06:38:32Z
user: login: Bhpsngum
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-13T09:54:13Z
user: login: mjwwit
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-17T07:51:14Z

Review comments:
diff_hunk: @@ -13,19 +13,94 @@ XHR object.
 Note: use the lowercase string "xmlhttprequest-ssl" in your require(). On
 case-sensitive systems (eg Linux) using uppercase letters won't work.
 
-### Non-standard features ###
-
-Non-standard options for this module are passed through the `XMLHttpRequest` constructor. The following options control `https:` SSL requests: `ca`, `cert`, `ciphers`, `key`, `passphrase`, `pfx`, and `rejectUnauthorized`. You can find their functionality in the [Node.js docs](https://nodejs.org/api/https.html#httpsrequestoptions-callback).
-
-Additionally, the `agent` option allows you to specify a [Node.js Agent](https://nodejs.org/api/https.html#class-httpsagent) instance, allowing connection reuse.
-
-To prevent a process from not exiting naturally because a request socket from this module is still open, you can set `autoUnref` to a truthy value.
-
-This module allows control over the maximum number of redirects that are followed. You can set the `maxRedirects` option to do this. The default number is 20.
-
-Using the `allowFileSystemResources` option allows you to control access to the local filesystem through the `file:` protocol.
-
-The `origin` option allows you to set a base URL for the request. The resulting request URL will be constructed as follows `new URL(url, origin)`.
+## Non-standard features ##
+### Additional options ###
+
+Non-standard options for this module are passed through the `XMLHttpRequest` constructor. Here is the list of all options:
+
+<table>
+    <thead>
+        <tr>
+            <th>Option</th>
+            <th>Default value</th>
+            <th>Description</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td><code>ca</code></td>
+            <td rowspan="6"><code>undefined</code></td>
+            <td rowspan="7">Control <code>https:</code> requests, you can find their functionality in the <a href="https://nodejs.org/api/https.html#httpsrequestoptions-callback">Nodejs Documentation</a></td>
+        </tr>
+        <tr>
+            <td><code>cert</code></td>
+        </tr>
+        <tr>
+            <td><code>ciphers</code></td>
+        </tr>
+		<tr>
+            <td><code>key</code></td>
+        </tr>
+		<tr>
+            <td><code>passhphrase</code></td>
+        </tr>
+		<tr>
+            <td><code>pfx</code></td>
+        </tr>
+		<tr>
+            <td><code>rejectUnauthorized</code></td>
+			<td><code>true</code></td>
+        </tr>
+		<tr>
+            <td><code>agent</code></td>
+			<td><code>undefined</code></td>
+			<td>Allows to specify a <a href="https://nodejs.org/api/https.html#class-httpsagent">Nodejs Agent</a> instance, allowing connection reuse</td>
+        </tr>
+		<tr>
+            <td><code>autoUnref</code></td>
+			<td><code>false</code></td>
+			<td>Set to any truthy value to prevent a process from not exiting naturally because a request socket from this module is still open</td>
+        </tr>
+		<tr>
+            <td><code>maxRedirects</code></td>
+			<td><code>20</code></td>
+			<td>Allows control over the maximum number of redirects that are followed</td>
+        </tr>
+		<tr>
+            <td><code>allowFileSystemResources</code></td>
+			<td><code>true</code></td>
+			<td>Allows control access to the local filesystem through the <code>file:</code> protocol</td>
+        </tr>
+		<tr>
+            <td><code>origin</code></td>
+			<td><code>undefined</code></td>
+			<td>Set a base URL for the requests called using this instance. The resulting request URL will be constructed as follows: <code>new URL(url, origin)</code></td>
+        </tr>
+		<tr>
+            <td><code>syncPolicy</code></td>
+			<td><code>"warn"</code></td>
+			<td>Control feature behavior of the synchronous feature:<ul><li><code>"disabled"</code>: Disable the feature completely, throws error after calling <code>.send()</code> if in synchronous mode</li><li><code>"warn"</code>: Enable the feature, but show a warning when calling <code>.open()</code> with synchronous mode</li><li><code>"enabled"</code>: Enable the feature without showing any additional warnings or errors</li></ul></td>
+        </tr>
+		<tr>
+            <td><code>disableHeaderCheck</code></td>
+			<td><code>false</code></td>
+			<td>Disable the check against forbidden headers to be added to a XHR request</td>
+        </tr>
+		<tr>
+            <td><code>xmlParser</code></td>
+			<td>none</td>
+			<td>Specify a parser (non-async) to parse document from text when <code>xhr.responseType</code> is either <code>"document"</code> or in text format. If the parser is invalid or omitted, <code>xhr.responseXML</code> will be <code>null</code></td>
+        </tr>
+		<tr>
+            <td><code>textDecoder</code></td>
+			<td><code>TextDecoder</code> or <code>buf.toString(enc)</code> depending on Node version</td>
+			<td>Specify a text decoder (non-async), accepting a buffer <code>buf</code> and encoding <code>enc</code> to decode to desired encoding.<br>Note that <code>TextDecoder</code> at version 12 does not support a wide range of encodings than later node version does</td>
+        </tr>
+    </tbody>
+</table>
path: README.md
user: login: mjwwit
body: Github actually supports tables in markdown: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-tables. This would probably greatly enhance readability of the source file.
created_at: 2025-05-13T05:39:53Z
author_association: OWNER
diff_hunk: @@ -439,14 +562,18 @@ function XMLHttpRequest(opts) {
    * @param string data Optional data to send as request body.
    */
   this.send = function(data) {
-    if (this.readyState != this.OPENED) {
+    if (readyState != this.OPENED) {
       throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");
     }
 
     if (sendFlag) {
       throw new Error("INVALID_STATE_ERR: send has already been called");
     }
 
+    if (opts.syncPolicy === "disabled") {
+      throw new Error("Synchronous requests are disabled for this instance.");
+    }
path: lib/XMLHttpRequest.js
user: login: mjwwit
body: But this could still be an async XHR at this point right? Maybe the condition should be amended with `&& !settings.async`.
created_at: 2025-05-13T06:02:59Z
author_association: OWNER
diff_hunk: @@ -10,7 +10,7 @@ var server = http.createServer(function (req, res) {
   // Test non-conforming allowed header
   assert.equal("node-XMLHttpRequest-test", req.headers["user-agent"]);
   // Test header set with blacklist disabled
-  assert.equal("http://github.com", req.headers["referer"]);
+  // assert.equal("http://github.com", req.headers["referer"]);
path: tests/test-headers.js
user: login: mjwwit
body: We should either remove these assertions or re-enable them
created_at: 2025-05-13T06:10:25Z
author_association: OWNER
diff_hunk: @@ -13,19 +13,94 @@ XHR object.
 Note: use the lowercase string "xmlhttprequest-ssl" in your require(). On
 case-sensitive systems (eg Linux) using uppercase letters won't work.
 
-### Non-standard features ###
-
-Non-standard options for this module are passed through the `XMLHttpRequest` constructor. The following options control `https:` SSL requests: `ca`, `cert`, `ciphers`, `key`, `passphrase`, `pfx`, and `rejectUnauthorized`. You can find their functionality in the [Node.js docs](https://nodejs.org/api/https.html#httpsrequestoptions-callback).
-
-Additionally, the `agent` option allows you to specify a [Node.js Agent](https://nodejs.org/api/https.html#class-httpsagent) instance, allowing connection reuse.
-
-To prevent a process from not exiting naturally because a request socket from this module is still open, you can set `autoUnref` to a truthy value.
-
-This module allows control over the maximum number of redirects that are followed. You can set the `maxRedirects` option to do this. The default number is 20.
-
-Using the `allowFileSystemResources` option allows you to control access to the local filesystem through the `file:` protocol.
-
-The `origin` option allows you to set a base URL for the request. The resulting request URL will be constructed as follows `new URL(url, origin)`.
+## Non-standard features ##
+### Additional options ###
+
+Non-standard options for this module are passed through the `XMLHttpRequest` constructor. Here is the list of all options:
+
+<table>
+    <thead>
+        <tr>
+            <th>Option</th>
+            <th>Default value</th>
+            <th>Description</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td><code>ca</code></td>
+            <td rowspan="6"><code>undefined</code></td>
+            <td rowspan="7">Control <code>https:</code> requests, you can find their functionality in the <a href="https://nodejs.org/api/https.html#httpsrequestoptions-callback">Nodejs Documentation</a></td>
+        </tr>
+        <tr>
+            <td><code>cert</code></td>
+        </tr>
+        <tr>
+            <td><code>ciphers</code></td>
+        </tr>
+		<tr>
+            <td><code>key</code></td>
+        </tr>
+		<tr>
+            <td><code>passhphrase</code></td>
+        </tr>
+		<tr>
+            <td><code>pfx</code></td>
+        </tr>
+		<tr>
+            <td><code>rejectUnauthorized</code></td>
+			<td><code>true</code></td>
+        </tr>
+		<tr>
+            <td><code>agent</code></td>
+			<td><code>undefined</code></td>
+			<td>Allows to specify a <a href="https://nodejs.org/api/https.html#class-httpsagent">Nodejs Agent</a> instance, allowing connection reuse</td>
+        </tr>
+		<tr>
+            <td><code>autoUnref</code></td>
+			<td><code>false</code></td>
+			<td>Set to any truthy value to prevent a process from not exiting naturally because a request socket from this module is still open</td>
+        </tr>
+		<tr>
+            <td><code>maxRedirects</code></td>
+			<td><code>20</code></td>
+			<td>Allows control over the maximum number of redirects that are followed</td>
+        </tr>
+		<tr>
+            <td><code>allowFileSystemResources</code></td>
+			<td><code>true</code></td>
+			<td>Allows control access to the local filesystem through the <code>file:</code> protocol</td>
+        </tr>
+		<tr>
+            <td><code>origin</code></td>
+			<td><code>undefined</code></td>
+			<td>Set a base URL for the requests called using this instance. The resulting request URL will be constructed as follows: <code>new URL(url, origin)</code></td>
+        </tr>
+		<tr>
+            <td><code>syncPolicy</code></td>
+			<td><code>"warn"</code></td>
+			<td>Control feature behavior of the synchronous feature:<ul><li><code>"disabled"</code>: Disable the feature completely, throws error after calling <code>.send()</code> if in synchronous mode</li><li><code>"warn"</code>: Enable the feature, but show a warning when calling <code>.open()</code> with synchronous mode</li><li><code>"enabled"</code>: Enable the feature without showing any additional warnings or errors</li></ul></td>
+        </tr>
+		<tr>
+            <td><code>disableHeaderCheck</code></td>
+			<td><code>false</code></td>
+			<td>Disable the check against forbidden headers to be added to a XHR request</td>
+        </tr>
+		<tr>
+            <td><code>xmlParser</code></td>
+			<td>none</td>
+			<td>Specify a parser (non-async) to parse document from text when <code>xhr.responseType</code> is either <code>"document"</code> or in text format. If the parser is invalid or omitted, <code>xhr.responseXML</code> will be <code>null</code></td>
+        </tr>
+		<tr>
+            <td><code>textDecoder</code></td>
+			<td><code>TextDecoder</code> or <code>buf.toString(enc)</code> depending on Node version</td>
+			<td>Specify a text decoder (non-async), accepting a buffer <code>buf</code> and encoding <code>enc</code> to decode to desired encoding.<br>Note that <code>TextDecoder</code> at version 12 does not support a wide range of encodings than later node version does</td>
+        </tr>
+    </tbody>
+</table>
path: README.md
user: login: Bhpsngum
body: problem is the colspan and rowspan in the table i specified to group similar descriptions, which GitHub Markdown doesn't support yet :(
created_at: 2025-05-13T06:30:24Z
author_association: NONE
diff_hunk: @@ -10,7 +10,7 @@ var server = http.createServer(function (req, res) {
   // Test non-conforming allowed header
   assert.equal("node-XMLHttpRequest-test", req.headers["user-agent"]);
   // Test header set with blacklist disabled
-  assert.equal("http://github.com", req.headers["referer"]);
+  // assert.equal("http://github.com", req.headers["referer"]);
path: tests/test-headers.js
user: login: Bhpsngum
body: i need to rewrite this test with our new `disableHeaderCheck` flag added, i forgot about that 😅 
created_at: 2025-05-13T06:31:48Z
author_association: NONE
diff_hunk: @@ -13,19 +13,94 @@ XHR object.
 Note: use the lowercase string "xmlhttprequest-ssl" in your require(). On
 case-sensitive systems (eg Linux) using uppercase letters won't work.
 
-### Non-standard features ###
-
-Non-standard options for this module are passed through the `XMLHttpRequest` constructor. The following options control `https:` SSL requests: `ca`, `cert`, `ciphers`, `key`, `passphrase`, `pfx`, and `rejectUnauthorized`. You can find their functionality in the [Node.js docs](https://nodejs.org/api/https.html#httpsrequestoptions-callback).
-
-Additionally, the `agent` option allows you to specify a [Node.js Agent](https://nodejs.org/api/https.html#class-httpsagent) instance, allowing connection reuse.
-
-To prevent a process from not exiting naturally because a request socket from this module is still open, you can set `autoUnref` to a truthy value.
-
-This module allows control over the maximum number of redirects that are followed. You can set the `maxRedirects` option to do this. The default number is 20.
-
-Using the `allowFileSystemResources` option allows you to control access to the local filesystem through the `file:` protocol.
-
-The `origin` option allows you to set a base URL for the request. The resulting request URL will be constructed as follows `new URL(url, origin)`.
+## Non-standard features ##
+### Additional options ###
+
+Non-standard options for this module are passed through the `XMLHttpRequest` constructor. Here is the list of all options:
+
+<table>
+    <thead>
+        <tr>
+            <th>Option</th>
+            <th>Default value</th>
+            <th>Description</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td><code>ca</code></td>
+            <td rowspan="6"><code>undefined</code></td>
+            <td rowspan="7">Control <code>https:</code> requests, you can find their functionality in the <a href="https://nodejs.org/api/https.html#httpsrequestoptions-callback">Nodejs Documentation</a></td>
+        </tr>
+        <tr>
+            <td><code>cert</code></td>
+        </tr>
+        <tr>
+            <td><code>ciphers</code></td>
+        </tr>
+		<tr>
+            <td><code>key</code></td>
+        </tr>
+		<tr>
+            <td><code>passhphrase</code></td>
+        </tr>
+		<tr>
+            <td><code>pfx</code></td>
+        </tr>
+		<tr>
+            <td><code>rejectUnauthorized</code></td>
+			<td><code>true</code></td>
+        </tr>
+		<tr>
+            <td><code>agent</code></td>
+			<td><code>undefined</code></td>
+			<td>Allows to specify a <a href="https://nodejs.org/api/https.html#class-httpsagent">Nodejs Agent</a> instance, allowing connection reuse</td>
+        </tr>
+		<tr>
+            <td><code>autoUnref</code></td>
+			<td><code>false</code></td>
+			<td>Set to any truthy value to prevent a process from not exiting naturally because a request socket from this module is still open</td>
+        </tr>
+		<tr>
+            <td><code>maxRedirects</code></td>
+			<td><code>20</code></td>
+			<td>Allows control over the maximum number of redirects that are followed</td>
+        </tr>
+		<tr>
+            <td><code>allowFileSystemResources</code></td>
+			<td><code>true</code></td>
+			<td>Allows control access to the local filesystem through the <code>file:</code> protocol</td>
+        </tr>
+		<tr>
+            <td><code>origin</code></td>
+			<td><code>undefined</code></td>
+			<td>Set a base URL for the requests called using this instance. The resulting request URL will be constructed as follows: <code>new URL(url, origin)</code></td>
+        </tr>
+		<tr>
+            <td><code>syncPolicy</code></td>
+			<td><code>"warn"</code></td>
+			<td>Control feature behavior of the synchronous feature:<ul><li><code>"disabled"</code>: Disable the feature completely, throws error after calling <code>.send()</code> if in synchronous mode</li><li><code>"warn"</code>: Enable the feature, but show a warning when calling <code>.open()</code> with synchronous mode</li><li><code>"enabled"</code>: Enable the feature without showing any additional warnings or errors</li></ul></td>
+        </tr>
+		<tr>
+            <td><code>disableHeaderCheck</code></td>
+			<td><code>false</code></td>
+			<td>Disable the check against forbidden headers to be added to a XHR request</td>
+        </tr>
+		<tr>
+            <td><code>xmlParser</code></td>
+			<td>none</td>
+			<td>Specify a parser (non-async) to parse document from text when <code>xhr.responseType</code> is either <code>"document"</code> or in text format. If the parser is invalid or omitted, <code>xhr.responseXML</code> will be <code>null</code></td>
+        </tr>
+		<tr>
+            <td><code>textDecoder</code></td>
+			<td><code>TextDecoder</code> or <code>buf.toString(enc)</code> depending on Node version</td>
+			<td>Specify a text decoder (non-async), accepting a buffer <code>buf</code> and encoding <code>enc</code> to decode to desired encoding.<br>Note that <code>TextDecoder</code> at version 12 does not support a wide range of encodings than later node version does</td>
+        </tr>
+    </tbody>
+</table>
path: README.md
user: login: mjwwit
body: Ah that sucks.. In that case we'll keep it like this.
created_at: 2025-05-13T06:38:32Z
author_association: OWNER
diff_hunk: @@ -439,14 +562,18 @@ function XMLHttpRequest(opts) {
    * @param string data Optional data to send as request body.
    */
   this.send = function(data) {
-    if (this.readyState != this.OPENED) {
+    if (readyState != this.OPENED) {
       throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");
     }
 
     if (sendFlag) {
       throw new Error("INVALID_STATE_ERR: send has already been called");
     }
 
+    if (opts.syncPolicy === "disabled") {
+      throw new Error("Synchronous requests are disabled for this instance.");
+    }
path: lib/XMLHttpRequest.js
user: login: Bhpsngum
body: yeah i forgot, thanks for the fixes
created_at: 2025-05-13T09:54:13Z
author_association: NONE

PR comments:
user: login: Bhpsngum
created_at: 2025-05-08T14:15:28Z
body: A few note on this: Since `TextDecoder` only adds support for more encodings on later node versions after v12, the test on v12 failed

We can either increase the minimum node version limit or change to use an external dependency for decoding (such as `iconv-lite`), there is a new option called `textEncoder` so user can pass their custom decoder into.
author_association: NONE
user: login: mjwwit
created_at: 2025-05-12T06:31:35Z
body: At first glance this looks really solid, thanks for the great work! :heart:
I'll have a more detailed look in the coming days, but we can probably get this merged this week.

> We can either increase the minimum node version limit or change to use an external dependency for decoding (such as `iconv-lite`), there is a new option called `textEncoder` so user can pass their custom decoder into.

I have no problems raising the minimum version to 13 or 14.
author_association: OWNER
user: login: Bhpsngum
created_at: 2025-05-12T07:43:06Z
body: I have no idea if we can keep the minimum version as v12, with caveats that encoding won't work with many options and need to specify a custom decoder (`textDecoder: function (buf, enc) { return iconv.decode(buf, enc) }` for example), or we raise it to the version where it already supports more options.

Please let me know what you think about this.
author_association: NONE
user: login: mjwwit
created_at: 2025-05-13T05:38:16Z
body: > I have no idea if we can keep the minimum version as v12, with caveats that encoding won't work with many options and need to specify a custom decoder (`textDecoder: function (buf, enc) { return iconv.decode(buf, enc) }` for example), or we raise it to the version where it already supports more options.
> 
> Please let me know what you think about this.

Keeping the minimum version with caveats is probably a bad idea. People will probably not read about these caveats proactively, and the package won't be fully functional on Node.js 12. I'll make the next release a major. People can still choose not to upgrade because of that.
author_association: OWNER
user: login: Bhpsngum
created_at: 2025-05-13T05:59:39Z
body: alright, that sounds good for me too 👍 
author_association: NONE
user: login: Bhpsngum
created_at: 2025-05-13T13:58:27Z
body: I've fixed the problems you addressed, and went further and added a basic measure to anti-prototype pollution, there is also a new test i added for that purpose (`test-pollution.js`).
author_association: NONE
user: login: mjwwit
created_at: 2025-05-14T17:39:25Z
body: It looks complete now, I'll run some usage tests to see if I can discover any problems. I'm not expecting to find anything, so I'll probably merge quickly after that. The release will follow shortly after that, probably on the same day.
author_association: OWNER
user: login: Bhpsngum
created_at: 2025-05-14T20:28:15Z
body: Alright, all sounds good to me 👍 
author_association: NONE
user: login: mjwwit
created_at: 2025-05-17T08:01:12Z
body: Thanks again for the solid work :heart:. I've released it as version 4.0.0.
author_association: OWNER