Skip to content

Commit

Permalink
Merge pull request #1219 from cprecioso/task/1216-fix-response-clone
Browse files Browse the repository at this point in the history
#1216@patch: Clone response body when cloning a response.
  • Loading branch information
capricorn86 authored Mar 9, 2024
2 parents 4c808b6 + 1559f62 commit 235d06f
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 73 deletions.
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
59 changes: 25 additions & 34 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 { 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';
Expand All @@ -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');

Expand Down Expand Up @@ -545,7 +545,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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
Expand All @@ -567,7 +570,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
);
(<boolean>this.response.redirected) = this.redirectCount > 0;
(<string>this.response.url) = this.request.url;
this.resolve(this.response);
Expand Down Expand Up @@ -599,15 +605,21 @@ export default class Fetch {
});
}

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 +635,21 @@ 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
);
(<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 +824,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);
});
}
});
}
}
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
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;
}
}
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
Loading

0 comments on commit 235d06f

Please sign in to comment.