From 4bfb12e6eae31e385aaa8bf4aca26932112d11f5 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Thu, 4 Jan 2024 20:54:12 -0800 Subject: [PATCH] Fix mangling responses --- src/interceptor/NodeClientRequest.ts | 12 ++- src/interceptor/utils/cloneIncomingMessage.ts | 74 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/interceptor/utils/cloneIncomingMessage.ts diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index 935c36d..1480489 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -12,6 +12,7 @@ import { IsomorphicRequest } from './utils/IsomorphicRequest'; import { getArrayBuffer } from './utils/bufferUtils'; import { isInterceptable } from './utils/isInterceptable'; import { IsomorphicResponse } from './utils/IsomorphicResponse'; +import { cloneIncomingMessage } from './utils/cloneIncomingMessage'; export type NodeClientOptions = { emitter: EventEmitter; @@ -106,7 +107,12 @@ export class NodeClientRequest extends ClientRequest { emit(event: 'pipe', src: Readable): boolean; emit(event: 'unpipe', src: Readable): boolean; emit(event: string | symbol, ...args: any[]): boolean { + if (event === 'response') { + const response = args[0] as IncomingMessage; + const firstClone = cloneIncomingMessage(response); + const secondClone = cloneIncomingMessage(response); + async function emitResponse( requestId: string, message: IncomingMessage, @@ -119,12 +125,10 @@ export class NodeClientRequest extends ClientRequest { } if (this.isInterceptable) { - emitResponse(this.requestId as string, args[0], this.emitter); + emitResponse(this.requestId as string, secondClone, this.emitter); } - if (this.isInterceptable) { - emitResponse(this.requestId as string, args[0], this.emitter); - } + return super.emit(event as string, firstClone, ...args.slice(1)); } return super.emit(event as string, ...args); diff --git a/src/interceptor/utils/cloneIncomingMessage.ts b/src/interceptor/utils/cloneIncomingMessage.ts new file mode 100644 index 0000000..35b21ac --- /dev/null +++ b/src/interceptor/utils/cloneIncomingMessage.ts @@ -0,0 +1,74 @@ +import { IncomingMessage } from 'http' +import { PassThrough } from 'stream' + +export const IS_CLONE = Symbol('isClone') + +export interface ClonedIncomingMessage extends IncomingMessage { + [IS_CLONE]: boolean +} + +/** + * Clones a given `http.IncomingMessage` instance. + */ +export function cloneIncomingMessage( + message: IncomingMessage +): ClonedIncomingMessage { + const clone = message.pipe(new PassThrough()) + + // Inherit all direct "IncomingMessage" properties. + inheritProperties(message, clone) + + // Deeply inherit the message prototypes (Readable, Stream, EventEmitter, etc.). + const clonedPrototype = Object.create(IncomingMessage.prototype) + getPrototypes(clone).forEach((prototype) => { + inheritProperties(prototype, clonedPrototype) + }) + Object.setPrototypeOf(clone, clonedPrototype) + + Object.defineProperty(clone, IS_CLONE, { + enumerable: true, + value: true, + }) + + return clone as unknown as ClonedIncomingMessage +} + +/** + * Returns a list of all prototypes the given object extends. + */ +function getPrototypes(source: object): object[] { + const prototypes: object[] = [] + let current = source + + while ((current = Object.getPrototypeOf(current))) { + prototypes.push(current) + } + + return prototypes +} + +/** + * Inherits a given target object properties and symbols + * onto the given source object. + * @param source Object which should acquire properties. + * @param target Object to inherit the properties from. + */ +function inheritProperties(source: object, target: object): void { + const properties = [ + ...Object.getOwnPropertyNames(source), + ...Object.getOwnPropertySymbols(source), + ] + + for (const property of properties) { + if (target.hasOwnProperty(property)) { + continue + } + + const descriptor = Object.getOwnPropertyDescriptor(source, property) + if (!descriptor) { + continue + } + + Object.defineProperty(target, property, descriptor) + } +}