diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 74f9963c..bc9fcf70 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -151,3 +151,4 @@ export const host = Symbol('host'); export const setURL = Symbol('setURL'); export const localName = Symbol('localName'); export const registedClass = Symbol('registedClass'); +export const nodeStream = Symbol('nodeStream'); diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index a82d5499..a925db86 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -14,7 +14,6 @@ import { Socket } from 'net'; import Stream from 'stream'; import DataURIParser from './data-uri/DataURIParser.js'; import FetchCORSUtility from './utilities/FetchCORSUtility.js'; -import { ReadableStream } from 'stream/web'; import Request from './Request.js'; import Response from './Response.js'; import Event from '../event/Event.js'; @@ -28,6 +27,7 @@ import FetchResponseRedirectUtility from './utilities/FetchResponseRedirectUtili import FetchResponseHeaderUtility from './utilities/FetchResponseHeaderUtility.js'; import FetchHTTPSCertificate from './certificate/FetchHTTPSCertificate.js'; import { Buffer } from 'buffer'; +import FetchBodyUtility from './utilities/FetchBodyUtility.js'; const LAST_CHUNK = Buffer.from('0\r\n\r\n'); @@ -123,7 +123,11 @@ export default class Fetch { this.#window.location.protocol === 'https:' ) { throw new DOMException( - `Mixed Content: The page at '${this.#window.location.href}' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${this.request.url}'. This request has been blocked; the content must be served over HTTPS.`, + `Mixed Content: The page at '${ + this.#window.location.href + }' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint '${ + this.request.url + }'. This request has been blocked; the content must be served over HTTPS.`, DOMExceptionNameEnum.securityError ); } @@ -545,7 +549,10 @@ export default class Fetch { nodeResponse.statusCode === 204 || nodeResponse.statusCode === 304 ) { - this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions); + this.response = new this.#window.Response( + FetchBodyUtility.nodeToWebStream(body), + responseOptions + ); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -567,7 +574,10 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions); + this.response = new this.#window.Response( + FetchBodyUtility.nodeToWebStream(body), + responseOptions + ); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -599,7 +609,10 @@ export default class Fetch { }); } - this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions); + this.response = new this.#window.Response( + FetchBodyUtility.nodeToWebStream(body), + responseOptions + ); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -607,7 +620,10 @@ export default class Fetch { raw.on('end', () => { // Some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted. if (!this.response) { - this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions); + this.response = new this.#window.Response( + FetchBodyUtility.nodeToWebStream(body), + responseOptions + ); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -623,7 +639,10 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions); + this.response = new this.#window.Response( + FetchBodyUtility.nodeToWebStream(body), + responseOptions + ); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -631,7 +650,10 @@ export default class Fetch { } // Otherwise, use response as is - this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions); + this.response = new this.#window.Response( + FetchBodyUtility.nodeToWebStream(body), + responseOptions + ); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -806,31 +828,4 @@ export default class Fetch { this.reject(error); } } - - /** - * Wraps a Node.js stream into a browser-compatible ReadableStream. - * - * Enables the use of Node.js streams where browser ReadableStreams are required. - * Handles 'data', 'end', and 'error' events from the Node.js stream. - * - * @param nodeStream The Node.js stream to be converted. - * @returns ReadableStream - */ - private nodeToWebStream(nodeStream: Stream): ReadableStream { - return new ReadableStream({ - start(controller) { - nodeStream.on('data', (chunk) => { - controller.enqueue(chunk); - }); - - nodeStream.on('end', () => { - controller.close(); - }); - - nodeStream.on('error', (err) => { - controller.error(err); - }); - } - }); - } } diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index bf0184ce..09838782 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -275,12 +275,7 @@ export default class Response implements IResponse { headers: this.headers }); - (response.status) = this.status; - (response.statusText) = this.statusText; (response.ok) = this.ok; - (response.headers) = new Headers(this.headers); - (response.body) = this.body; - (response.bodyUsed) = this.bodyUsed; (response.redirected) = this.redirected; (response.type) = this.type; (response.url) = this.url; diff --git a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts index d203fc3c..555be013 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts @@ -9,29 +9,12 @@ import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import IRequestBody from '../types/IRequestBody.js'; import IResponseBody from '../types/IResponseBody.js'; import { Buffer } from 'buffer'; +import Stream from 'stream'; /** * Fetch body utility. */ export default class FetchBodyUtility { - /** - * Wraps a given value in a browser ReadableStream. - * - * This method creates a ReadableStream and immediately enqueues and closes it - * with the provided value, useful for stream API compatibility. - * - * @param value The value to be wrapped in a ReadableStream. - * @returns ReadableStream - */ - public static toReadableStream(value): ReadableStream { - return new ReadableStream({ - start(controller) { - controller.enqueue(value); - controller.close(); - } - }); - } - /** * Parses body and returns stream and type. * @@ -115,8 +98,8 @@ export default class FetchBodyUtility { * It creates a pass through stream and pipes the original stream to it. * * @param requestOrResponse Request or Response. - * @param requestOrResponse.body - * @param requestOrResponse.bodyUsed + * @param requestOrResponse.body Body. + * @param requestOrResponse.bodyUsed Body used. * @returns New stream. */ public static cloneBodyStream(requestOrResponse: { @@ -130,7 +113,25 @@ export default class FetchBodyUtility { ); } + // If a buffer is set, use it to create a new stream. + if (requestOrResponse[PropertySymbol.buffer]) { + return this.toReadableStream(requestOrResponse[PropertySymbol.buffer]); + } + + // Pipe underlying node stream if it exists. + if (requestOrResponse.body[PropertySymbol.nodeStream]) { + const stream1 = new Stream.PassThrough(); + const stream2 = new Stream.PassThrough(); + requestOrResponse.body[PropertySymbol.nodeStream].pipe(stream1); + requestOrResponse.body[PropertySymbol.nodeStream].pipe(stream2); + // Sets the body of the cloned request/response to the first pass through stream. + requestOrResponse.body = this.nodeToWebStream(stream1); + // Returns the clone. + return this.nodeToWebStream(stream2); + } + // Uses the tee() method to clone the ReadableStream + // This requires the stream to be consumed in parallel which is not the case for the fetch API const [stream1, stream2] = requestOrResponse.body.tee(); // Sets the body of the cloned request to the first pass through stream. @@ -198,4 +199,50 @@ export default class FetchBodyUtility { ); } } + /** + * Wraps a given value in a browser ReadableStream. + * + * This method creates a ReadableStream and immediately enqueues and closes it + * with the provided value, useful for stream API compatibility. + * + * @param value The value to be wrapped in a ReadableStream. + * @returns ReadableStream + */ + public static toReadableStream(value): ReadableStream { + return new ReadableStream({ + start(controller) { + controller.enqueue(value); + controller.close(); + } + }); + } + + /** + * Wraps a Node.js stream into a browser-compatible ReadableStream. + * + * Enables the use of Node.js streams where browser ReadableStreams are required. + * Handles 'data', 'end', and 'error' events from the Node.js stream. + * + * @param nodeStream The Node.js stream to be converted. + * @returns ReadableStream + */ + public static nodeToWebStream(nodeStream: Stream): ReadableStream { + const readableStream = new ReadableStream({ + start(controller) { + nodeStream.on('data', (chunk) => { + controller.enqueue(chunk); + }); + + nodeStream.on('end', () => { + controller.close(); + }); + + nodeStream.on('error', (err) => { + controller.error(err); + }); + } + }); + readableStream[PropertySymbol.nodeStream] = nodeStream; + return readableStream; + } }