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

fix(cloneIncomingMessage): inherit prototype properties #214

Merged
merged 5 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 12 additions & 7 deletions src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { Socket } from 'net'
import { IncomingMessage } from 'http'
import { Stream, Readable, EventEmitter } from 'stream'
import { cloneIncomingMessage, IS_CLONE } from './cloneIncomingMessage'

test('clones a given IncomingMessage', () => {
const source = new IncomingMessage(new Socket())
source.statusCode = 200
source.statusMessage = 'OK'
source.headers = { 'x-powered-by': 'msw' }
const clone = cloneIncomingMessage(source)
const message = new IncomingMessage(new Socket())
message.statusCode = 200
message.statusMessage = 'OK'
message.headers = { 'x-powered-by': 'msw' }
const clone = cloneIncomingMessage(message)

// Prototypes must be preserved.
expect(clone).toBeInstanceOf(IncomingMessage)
expect(clone).toBeInstanceOf(EventEmitter)
expect(clone).toBeInstanceOf(Stream)
expect(clone).toBeInstanceOf(Readable)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding assertions to check that the cloned message indeed extends all the prototypes of the original message.


expect(clone.statusCode).toEqual(200)
expect(clone.statusMessage).toEqual('OK')
expect(clone.headers).toHaveProperty('x-powered-by', 'msw')

// Cloned IncomingMessage must be marked respectively.
expect(clone[IS_CLONE]).toEqual(true)

expect(clone).toHaveProperty('_events')
})
75 changes: 54 additions & 21 deletions src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,68 @@ export interface ClonedIncomingMessage extends IncomingMessage {
[IS_CLONE]: boolean
}

/**
* Clones a given `http.IncomingMessage` instance.
*/
export function cloneIncomingMessage(
message: IncomingMessage
): ClonedIncomingMessage {
const stream = message.pipe(new PassThrough())
const properties = [
...Object.getOwnPropertyNames(message),
...Object.getOwnPropertySymbols(message),
] as Array<keyof IncomingMessage>

for (const propertyName of properties) {
if (stream.hasOwnProperty(propertyName)) {
continue
}
const clone = message.pipe(new PassThrough())

const propertyDescriptor = Object.getOwnPropertyDescriptor(
message,
propertyName
)
// Inherit all direct "IncomingMessage" properties.
inheritProperties(message, clone)

Object.defineProperty(stream, propertyName, {
...propertyDescriptor,
value: message[propertyName],
})
}
// 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(stream, IS_CLONE, {
Object.defineProperty(clone, IS_CLONE, {
enumerable: true,
value: true,
})

return stream as unknown as ClonedIncomingMessage
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)
}
}