From 857bd480e11aa457aae98596a908ed5b82ea94ea Mon Sep 17 00:00:00 2001 From: Mas0nShi Date: Thu, 23 Feb 2023 16:42:52 +0800 Subject: [PATCH] #783@patch: Fixes XMLHttpRequest response encoding error. --- packages/happy-dom/package.json | 3 +- .../src/xml-http-request/XMLHttpRequest.ts | 40 ++++++++++++++----- .../XMLHttpRequestSyncRequestScriptBuilder.ts | 1 - 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json index cae78eddc..40086a71c 100644 --- a/packages/happy-dom/package.json +++ b/packages/happy-dom/package.json @@ -47,7 +47,8 @@ "node-fetch": "^2.x.x", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0" + "whatwg-mimetype": "^3.0.0", + "iconv-lite": "^0.6.3" }, "devDependencies": { "@types/he": "^1.1.2", diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 9fc13d972..5ae8edcfe 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -17,6 +17,8 @@ import ProgressEvent from '../event/events/ProgressEvent'; import XMLHttpResponseTypeEnum from './XMLHttpResponseTypeEnum'; import XMLHttpRequestCertificate from './XMLHttpRequestCertificate'; import XMLHttpRequestSyncRequestScriptBuilder from './utilities/XMLHttpRequestSyncRequestScriptBuilder'; +import iconv from 'iconv-lite'; +import { Buffer } from 'buffer'; // These headers are not user setable. // The following are allowed but banned in the spec: @@ -46,6 +48,8 @@ const FORBIDDEN_REQUEST_HEADERS = [ // These request methods are not allowed const FORBIDDEN_REQUEST_METHODS = ['TRACE', 'TRACK', 'CONNECT']; +// Match Content-Type header charset +const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i; /** * XMLHttpRequest. @@ -595,8 +599,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this._state.status = response.statusCode; this._state.statusText = response.statusMessage; // Sync responseType === '' - this._state.response = response.text; - this._state.responseText = response.text; + this._state.response = this._decodeResponseText(Buffer.from(response.data, 'base64')); + this._state.responseText = this._state.response; this._state.responseXML = null; this._state.responseURL = RelativeURL.getAbsoluteURL( this._ownerDocument.defaultView.location, @@ -755,10 +759,6 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return; } - if (this._state.incommingMessage && this._state.incommingMessage.setEncoding) { - this._state.incommingMessage.setEncoding('utf-8'); - } - this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); this._state.status = this._state.incommingMessage.statusCode; this._state.statusText = this._state.incommingMessage.statusMessage; @@ -779,7 +779,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const contentLength = Number(this._state.incommingMessage.headers['content-length']); this.dispatchEvent( new ProgressEvent('progress', { - lengthComputable: isNaN(contentLength) ? false : true, + lengthComputable: !isNaN(contentLength), loaded: tempResponse.length, total: isNaN(contentLength) ? 0 : contentLength }) @@ -954,7 +954,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.json: try { return { - response: JSON.parse(data.toString()), + response: JSON.parse(this._decodeResponseText(data)), responseText: null, responseXML: null }; @@ -964,9 +964,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.text: case '': default: + const responseText = this._decodeResponseText(data); return { - response: data.toString(), - responseText: data.toString(), + response: responseText, + responseText: responseText, responseXML: null }; } @@ -995,4 +996,23 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this._state.error = true; this._setState(XMLHttpRequestReadyStateEnum.done); } + + /** + * Decodes response text. + * + * @param data Data. + * @returns Decoded text. + **/ + private _decodeResponseText(data: Buffer): string { + const contextTypeEncodingRegexp = new RegExp(CONTENT_TYPE_ENCODING_REGEXP, 'gi'); + let contentType; + if (this._state.incommingMessage && this._state.incommingMessage.headers) { + contentType = this._state.incommingMessage.headers['content-type']; // For remote requests (http/https). + } else { + contentType = this._state.requestHeaders['content-type']; // For local requests or unpredictable remote requests. + } + const charset = contextTypeEncodingRegexp.exec(contentType); + // Default utf-8 + return iconv.decode(data, charset ? charset[1] : 'utf-8'); + } } diff --git a/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts b/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts index 9e8910a1e..00fdb1861 100644 --- a/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts +++ b/packages/happy-dom/src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.ts @@ -23,7 +23,6 @@ export default class XMLHttpRequestSyncRequestScriptBuilder { const request = sendRequest(options, (response) => { let responseText = ''; let responseData = Buffer.alloc(0); - response.setEncoding('utf8'); response.on('data', (chunk) => { responseText += chunk; responseData = Buffer.concat([responseData, Buffer.from(chunk)]);