Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1216@patch: Clone response body when cloning a response. #1219

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
5 changes: 4 additions & 1 deletion packages/happy-dom/src/config/IHTMLElementTagNameMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import IHTMLVideoElement from '../nodes/html-video-element/IHTMLVideoElement.js'

// Makes it work with custom elements when they declare their own interface.
declare global {
/* eslint-disable-next-line @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-empty-interface */
interface HTMLElementTagNameMap {}
/* eslint-enable @typescript-eslint/naming-convention */
/* eslint-enable @typescript-eslint/no-empty-interface */
}

export default interface IHTMLElementTagNameMap extends HTMLElementTagNameMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@
return color
? {
'outline-color': { value: color, important }
}
}

Check warning on line 536 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `··` with `↹`

Check warning on line 536 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `··` with `↹`

Check warning on line 536 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `··` with `↹`
: null;
}

Expand Down Expand Up @@ -1417,7 +1417,7 @@
return color
? {
'border-top-color': { value: color, important }
}
}

Check warning on line 1420 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `··` with `↹`

Check warning on line 1420 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `··` with `↹`

Check warning on line 1420 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `··` with `↹`
: null;
}

Expand Down Expand Up @@ -1445,7 +1445,7 @@
return color
? {
'border-right-color': { value: color, important }
}
}

Check warning on line 1448 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `··` with `↹`

Check warning on line 1448 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `··` with `↹`

Check warning on line 1448 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `··` with `↹`
: null;
}

Expand Down Expand Up @@ -1473,7 +1473,7 @@
return color
? {
'border-bottom-color': { value: color, important }
}
}

Check warning on line 1476 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `··` with `↹`

Check warning on line 1476 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `··` with `↹`

Check warning on line 1476 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `··` with `↹`
: null;
}

Expand Down Expand Up @@ -1501,7 +1501,7 @@
return color
? {
'border-left-color': { value: color, important }
}
}

Check warning on line 1504 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `··` with `↹`

Check warning on line 1504 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `··` with `↹`

Check warning on line 1504 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `··` with `↹`
: null;
}

Expand Down Expand Up @@ -2769,7 +2769,7 @@
return color
? {
['background-color']: { important, value: color }
}
}

Check warning on line 2772 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `··` with `↹`

Check warning on line 2772 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `··` with `↹`

Check warning on line 2772 in packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `··` with `↹`
: null;
}

Expand Down
65 changes: 30 additions & 35 deletions packages/happy-dom/src/fetch/Fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
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';
Expand All @@ -28,6 +27,7 @@
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');

Expand Down Expand Up @@ -123,7 +123,11 @@
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 '${

Check warning on line 126 in packages/happy-dom/src/fetch/Fetch.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `⏎↹↹↹↹↹this.#window.location.href⏎↹↹↹↹}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${⏎↹↹↹↹↹this.request.url⏎↹↹↹↹` with `this.#window.location.href}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${this.request.url`

Check warning on line 126 in packages/happy-dom/src/fetch/Fetch.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `⏎↹↹↹↹↹this.#window.location.href⏎↹↹↹↹}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${⏎↹↹↹↹↹this.request.url⏎↹↹↹↹` with `this.#window.location.href}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${this.request.url`

Check warning on line 126 in packages/happy-dom/src/fetch/Fetch.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `⏎↹↹↹↹↹this.#window.location.href⏎↹↹↹↹}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${⏎↹↹↹↹↹this.request.url⏎↹↹↹↹` with `this.#window.location.href}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${this.request.url`
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
);
}
Expand Down Expand Up @@ -545,7 +549,10 @@
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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
Expand All @@ -567,7 +574,10 @@
// 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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
Expand Down Expand Up @@ -599,15 +609,21 @@
});
}

this.response = new this.#window.Response(this.nodeToWebStream(body), responseOptions);
this.response = new this.#window.Response(
FetchBodyUtility.nodeToWebStream(body),
responseOptions
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
});
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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
Expand All @@ -623,15 +639,21 @@
// 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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
return;
}

// 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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
Expand Down Expand Up @@ -806,31 +828,4 @@
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);
});
}
});
}
}
9 changes: 3 additions & 6 deletions packages/happy-dom/src/fetch/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,18 +267,15 @@ export default class Response implements IResponse {
* @returns Clone.
*/
public clone(): Response {
const response = new this.#window.Response(this.body, {
const body = FetchBodyUtility.cloneBodyStream(this);

const response = new this.#window.Response(body, {
status: this.status,
statusText: this.statusText,
headers: this.headers
});

(<number>response.status) = this.status;
(<string>response.statusText) = this.statusText;
(<boolean>response.ok) = this.ok;
(<Headers>response.headers) = new Headers(this.headers);
(<ReadableStream>response.body) = this.body;
(<boolean>response.bodyUsed) = this.bodyUsed;
(<boolean>response.redirected) = this.redirected;
(<string>response.type) = this.type;
(<string>response.url) = this.url;
Expand Down
6 changes: 5 additions & 1 deletion packages/happy-dom/src/fetch/SyncFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@
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 '${

Check warning on line 115 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `⏎↹↹↹↹↹this.#window.location.href⏎↹↹↹↹}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${⏎↹↹↹↹↹this.request.url⏎↹↹↹↹` with `this.#window.location.href}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${this.request.url`

Check warning on line 115 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `⏎↹↹↹↹↹this.#window.location.href⏎↹↹↹↹}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${⏎↹↹↹↹↹this.request.url⏎↹↹↹↹` with `this.#window.location.href}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${this.request.url`

Check warning on line 115 in packages/happy-dom/src/fetch/SyncFetch.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `⏎↹↹↹↹↹this.#window.location.href⏎↹↹↹↹}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${⏎↹↹↹↹↹this.request.url⏎↹↹↹↹` with `this.#window.location.href}'·was·loaded·over·HTTPS,·but·requested·an·insecure·XMLHttpRequest·endpoint·'${this.request.url`
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
);
}
Expand Down
87 changes: 67 additions & 20 deletions packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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: {
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
}
5 changes: 3 additions & 2 deletions packages/happy-dom/src/file/FileReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,9 @@
case FileReaderFormatEnum.dataURL: {
// Spec seems very unclear here; see https://github.com/w3c/FileAPI/issues/104.
const contentType = WhatwgMIMEType.parse(blob.type) || 'application/octet-stream';
(<Buffer | ArrayBuffer | string>this.result) =
`data:${contentType};base64,${data.toString('base64')}`;
(<Buffer | ArrayBuffer | string>(

Check warning on line 165 in packages/happy-dom/src/file/FileReader.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `(⏎↹↹↹↹↹↹↹this.result` with `this.result)·=`

Check warning on line 165 in packages/happy-dom/src/file/FileReader.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `(⏎↹↹↹↹↹↹↹this.result` with `this.result)·=`

Check warning on line 165 in packages/happy-dom/src/file/FileReader.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `(⏎↹↹↹↹↹↹↹this.result` with `this.result)·=`
this.result
)) = `data:${contentType};base64,${data.toString('base64')}`;

Check warning on line 167 in packages/happy-dom/src/file/FileReader.ts

View workflow job for this annotation

GitHub Actions / build (16)

Replace `))·=·` with `↹`

Check warning on line 167 in packages/happy-dom/src/file/FileReader.ts

View workflow job for this annotation

GitHub Actions / build (18)

Replace `))·=·` with `↹`

Check warning on line 167 in packages/happy-dom/src/file/FileReader.ts

View workflow job for this annotation

GitHub Actions / build (20)

Replace `))·=·` with `↹`
break;
}
case FileReaderFormatEnum.text: {
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/location/Location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { URL } from 'url';
*/
export default class Location {
// Public properties
public [Symbol.toStringTag]: string = 'Location';
public [Symbol.toStringTag] = 'Location';

// Private properties
#browserFrame: IBrowserFrame;
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/match-media/MediaQueryParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ export default class MediaQueryParser {
? {
value: resolutionMatch[1],
operator: resolutionMatch[2]
}
}
: null,
type: resolutionMatch[3],
after: resolutionMatch[5]
? {
value: resolutionMatch[5],
operator: resolutionMatch[4]
}
}
: null
});
} else {
Expand Down
Loading
Loading