Skip to content

Commit

Permalink
Avoid polluting the original object in case of Object.create (#1797)
Browse files Browse the repository at this point in the history
Co-authored-by: Laurin Quast <laurinquast@googlemail.com>
  • Loading branch information
ardatan and n1ru4l authored Nov 13, 2024
1 parent cf87b23 commit 39da7bb
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-walls-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@whatwg-node/server': patch
---

Avoid polluting the original object in case of \`Object.create\`
44 changes: 36 additions & 8 deletions packages/server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,56 +529,82 @@ export function isolateObject<TIsolatedObject extends object>(
}
originalCtx = {} as TIsolatedObject;
}
const extraProps: Partial<TIsolatedObject> = {};
const deletedProps = new Set<string | symbol>();
const extraPropsByReceiver = new WeakMap<TIsolatedObject, Partial<TIsolatedObject>>();
const deletedPropsByReceiver = new WeakMap<TIsolatedObject, Set<string | symbol>>();
function getExtraProps(receiver: TIsolatedObject): any {
let extraProps = extraPropsByReceiver.get(receiver);
if (!extraProps) {
extraProps = {};
extraPropsByReceiver.set(receiver, extraProps);
}
return extraProps;
}
function getDeletedProps(receiver: TIsolatedObject) {
let deletedProps = deletedPropsByReceiver.get(receiver);
if (!deletedProps) {
deletedProps = new Set<string | symbol>();
deletedPropsByReceiver.set(receiver, deletedProps);
}
return deletedProps;
}
return new Proxy(originalCtx, {
get(originalCtx, prop) {
get(originalCtx, prop, receiver) {
if (waitUntilPromises != null && prop === 'waitUntil') {
return function waitUntil(promise: Promise<unknown>) {
waitUntilPromises.push(promise.catch(err => console.error(err)));
};
}
const extraPropVal = (extraProps as any)[prop];
const extraProps = getExtraProps(receiver);
const extraPropVal = extraProps[prop];
if (extraPropVal != null) {
if (typeof extraPropVal === 'function') {
return extraPropVal.bind(extraProps);
}
return extraPropVal;
}
const deletedProps = getDeletedProps(receiver);
if (deletedProps.has(prop)) {
return undefined;
}
return (originalCtx as any)[prop];
},
set(_originalCtx, prop, value) {
(extraProps as any)[prop] = value;
set(_originalCtx, prop, value, receiver) {
const extraProps = getExtraProps(receiver);
extraProps[prop] = value;
return true;
},
has(originalCtx, prop) {
if (waitUntilPromises != null && prop === 'waitUntil') {
return true;
}
const deletedProps = getDeletedProps(originalCtx);
if (deletedProps.has(prop)) {
return false;
}
const extraProps = getExtraProps(originalCtx);
if (prop in extraProps) {
return true;
}
return prop in originalCtx;
},
defineProperty(_originalCtx, prop, descriptor) {
defineProperty(originalCtx, prop, descriptor) {
const extraProps = getExtraProps(originalCtx);
return Reflect.defineProperty(extraProps, prop, descriptor);
},
deleteProperty(_originalCtx, prop) {
deleteProperty(originalCtx, prop) {
const extraProps = getExtraProps(originalCtx);
if (prop in extraProps) {
return Reflect.deleteProperty(extraProps, prop);
}
const deletedProps = getDeletedProps(originalCtx);
deletedProps.add(prop);
return true;
},
ownKeys(originalCtx) {
const extraProps = getExtraProps(originalCtx);
const extraKeys = Reflect.ownKeys(extraProps);
const originalKeys = Reflect.ownKeys(originalCtx);
const deletedProps = getDeletedProps(originalCtx);
const deletedKeys = Array.from(deletedProps);
const allKeys = new Set(
extraKeys.concat(originalKeys.filter(keys => !deletedKeys.includes(keys))),
Expand All @@ -589,9 +615,11 @@ export function isolateObject<TIsolatedObject extends object>(
return Array.from(allKeys);
},
getOwnPropertyDescriptor(originalCtx, prop) {
const extraProps = getExtraProps(originalCtx);
if (prop in extraProps) {
return Reflect.getOwnPropertyDescriptor(extraProps, prop);
}
const deletedProps = getDeletedProps(originalCtx);
if (deletedProps.has(prop)) {
return undefined;
}
Expand Down
16 changes: 9 additions & 7 deletions packages/server/src/uwebsockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,14 @@ export function getRequestFromUWSRequest({ req, res, fetchAPI, signal }: GetRequ
export function createWritableFromUWS(uwsResponse: UWSResponse, fetchAPI: FetchAPI) {
return new fetchAPI.WritableStream({
write(chunk) {
uwsResponse.write(chunk);
uwsResponse.cork(() => {
uwsResponse.write(chunk);
});
},
close() {
uwsResponse.end();
uwsResponse.cork(() => {
uwsResponse.end();
});
},
});
}
Expand Down Expand Up @@ -229,13 +233,11 @@ export function sendResponseToUwsOpts(
}
if (bufferOfRes) {
uwsResponse.end(bufferOfRes);
} else if (!fetchResponse.body) {
uwsResponse.end();
}
});
if (bufferOfRes) {
return;
}
if (!fetchResponse.body) {
uwsResponse.end();
if (bufferOfRes || !fetchResponse.body) {
return;
}
signal.addEventListener('abort', () => {
Expand Down
34 changes: 34 additions & 0 deletions packages/server/test/server-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,40 @@ describe('Server Context', () => {
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ foo: 'bar', bar: 'baz' });
});
it('Do not pollute the original object in case of `Object.create`', async () => {
const serverAdapter = createServerAdapter((_req, context0: any) => {
context0.i = 0;
const context1 = Object.create(context0);
context1.i = 1;
const context2 = Object.create(context0);
context2.i = 2;
return Response.json({
i0: context0.i,
i1: context1.i,
i2: context2.i,
});
});
const res = await serverAdapter.fetch('http://localhost');
const resJson = await res.json();
expect(resJson).toEqual({
i0: 0,
i1: 1,
i2: 2,
});
});
it('retains the prototype in case of `Object.create`', async () => {
class MyContext {}
const serverAdapter = createServerAdapter((_req, context0: MyContext) => {
return Response.json({
isMyContext: context0 instanceof MyContext,
});
});
const res = await serverAdapter.fetch('http://localhost', new MyContext());
const resJson = await res.json();
expect(resJson).toEqual({
isMyContext: true,
});
});
},
{ noLibCurl: true },
);
Expand Down
11 changes: 11 additions & 0 deletions packages/server/test/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { isolateObject } from '../src/utils';

describe('isolateObject', () => {
test('Object.create does not share property assignments', () => {
const origin = isolateObject({});
const a = Object.create(origin);
const b = Object.create(origin);
a.a = 1;
expect(b.a).toEqual(undefined);
});
});

0 comments on commit 39da7bb

Please sign in to comment.