diff --git a/.changeset/popular-deers-whisper.md b/.changeset/popular-deers-whisper.md new file mode 100644 index 00000000000..baecaca8289 --- /dev/null +++ b/.changeset/popular-deers-whisper.md @@ -0,0 +1,5 @@ +--- +"@whatwg-node/server": patch +--- + +Fix for undefined server context parts diff --git a/packages/server/src/createServerAdapter.ts b/packages/server/src/createServerAdapter.ts index 601c0b7c613..bd87608a98b 100644 --- a/packages/server/src/createServerAdapter.ts +++ b/packages/server/src/createServerAdapter.ts @@ -39,12 +39,7 @@ import { } from './uwebsockets.js'; async function handleWaitUntils(waitUntilPromises: Promise[]) { - const waitUntils = await Promise.allSettled(waitUntilPromises); - waitUntils.forEach(waitUntil => { - if (waitUntil.status === 'rejected') { - console.error(waitUntil.reason); - } - }); + await Promise.allSettled(waitUntilPromises); } type RequestContainer = { request: Request }; @@ -58,14 +53,6 @@ function isRequestAccessible(serverContext: any): serverContext is RequestContai } } -function addWaitUntil(serverContext: any, waitUntilPromises: Promise[]): void { - serverContext['waitUntil'] = function (promise: Promise | void) { - if (promise != null) { - waitUntilPromises.push(promise); - } - }; -} - export interface ServerAdapterOptions { plugins?: ServerAdapterPlugin[]; fetchAPI?: Partial; @@ -206,8 +193,10 @@ function createServerAdapter< const defaultServerContext = { req: nodeRequest, res: serverResponse, + waitUntil(cb: Promise) { + waitUntilPromises.push(cb.catch(err => console.error(err))); + }, }; - addWaitUntil(defaultServerContext, waitUntilPromises); let response$: Response | Promise | undefined; try { response$ = handleNodeRequest(nodeRequest, defaultServerContext as any, ...ctx); @@ -234,10 +223,15 @@ function createServerAdapter< const defaultServerContext = { res, req, + waitUntil(cb: Promise) { + waitUntilPromises.push(cb.catch(err => console.error(err))); + }, }; - addWaitUntil(defaultServerContext, waitUntilPromises); + const filteredCtxParts = ctx.filter(partCtx => partCtx != null); const serverContext = - ctx.length > 0 ? completeAssign(defaultServerContext, ...ctx) : defaultServerContext; + filteredCtxParts.length > 0 + ? completeAssign(defaultServerContext, ...ctx) + : defaultServerContext; const request = getRequestFromUWSRequest({ req, res, @@ -277,23 +271,32 @@ function createServerAdapter< if (!event.respondWith || !event.request) { throw new TypeError(`Expected FetchEvent, got ${event}`); } - const serverContext = ctx.length > 0 ? Object.assign({}, event, ...ctx) : event; + const filteredCtxParts = ctx.filter(partCtx => partCtx != null); + const serverContext = + filteredCtxParts.length > 0 + ? completeAssign({}, event, ...filteredCtxParts) + : isolateObject(event); const response$ = handleRequest(event.request, serverContext); event.respondWith(response$); } function handleRequestWithWaitUntil(request: Request, ...ctx: Partial[]) { - const serverContext = ctx.length > 1 ? completeAssign(...ctx) : isolateObject(ctx[0]); - if (serverContext.waitUntil == null) { - const waitUntilPromises: Promise[] = []; - addWaitUntil(serverContext, waitUntilPromises); - const response$ = handleRequest(request, serverContext); - if (waitUntilPromises.length > 0) { - return handleWaitUntils(waitUntilPromises).then(() => response$); - } - return response$; + const filteredCtxParts: any[] = ctx.filter(partCtx => partCtx != null); + let waitUntilPromises: Promise[] | undefined; + const serverContext = + filteredCtxParts.length > 1 + ? completeAssign(...filteredCtxParts) + : isolateObject( + filteredCtxParts[0], + filteredCtxParts[0] == null || filteredCtxParts[0].waitUntil == null + ? (waitUntilPromises = []) + : undefined, + ); + const response$ = handleRequest(request, serverContext); + if (waitUntilPromises?.length) { + return handleWaitUntils(waitUntilPromises).then(() => response$); } - return handleRequest(request, serverContext); + return response$; } const fetchFn: ServerAdapterObject['fetch'] = ( diff --git a/packages/server/src/utils.ts b/packages/server/src/utils.ts index 370adff63f6..7f8bbc00ec3 100644 --- a/packages/server/src/utils.ts +++ b/packages/server/src/utils.ts @@ -446,6 +446,7 @@ export function handleErrorFromRequestHandler(error: any, ResponseCtor: typeof R export function isolateObject( originalCtx: TIsolatedObject, + waitUntilPromises?: Promise[], ): TIsolatedObject { if (originalCtx == null) { return {} as TIsolatedObject; @@ -454,6 +455,11 @@ export function isolateObject( const deletedProps = new Set(); return new Proxy(originalCtx, { get(originalCtx, prop, receiver) { + if (waitUntilPromises != null && prop === 'waitUntil') { + return function waitUntil(promise: Promise) { + waitUntilPromises.push(promise.catch(err => console.error(err))); + }; + } if (prop in extraProps) { return Reflect.get(extraProps, prop, receiver); } @@ -468,6 +474,9 @@ export function isolateObject( return Reflect.set(extraProps, prop, value, receiver); }, has(originalCtx, prop) { + if (waitUntilPromises != null && prop === 'waitUntil') { + return true; + } if (deletedProps.has(prop)) { return false; } @@ -490,9 +499,13 @@ export function isolateObject( const extraKeys = Reflect.ownKeys(extraProps); const originalKeys = Reflect.ownKeys(originalCtx); const deletedKeys = Array.from(deletedProps); - return Array.from( - new Set(extraKeys.concat(originalKeys.filter(keys => !deletedKeys.includes(keys)))), + const allKeys = new Set( + extraKeys.concat(originalKeys.filter(keys => !deletedKeys.includes(keys))), ); + if (waitUntilPromises != null) { + allKeys.add('waitUntil'); + } + return Array.from(allKeys); }, getOwnPropertyDescriptor(originalCtx, prop) { if (prop in extraProps) { diff --git a/packages/server/test/server-context.spec.ts b/packages/server/test/server-context.spec.ts index e3efebc974d..90acf8d2346 100644 --- a/packages/server/test/server-context.spec.ts +++ b/packages/server/test/server-context.spec.ts @@ -1,3 +1,5 @@ +import { Request } from '@whatwg-node/fetch'; +import { Response } from '@whatwg-node/server'; import { createServerAdapter } from '../src/createServerAdapter'; describe('Server Context', () => { @@ -20,4 +22,13 @@ describe('Server Context', () => { expect(seenCtx.has(exampleStaticCtx)).toBe(false); expect(exampleStaticCtx.foo).toBe('bar'); }); + it('filters empty ctx', async () => { + const adapter = createServerAdapter(function handler(_req, ctx) { + return Response.json(ctx); + }); + const ctxParts: any[] = [undefined, undefined, { foo: 'bar' }, undefined, { bar: 'baz' }]; + const res = await adapter(new Request('https://example.com'), ...ctxParts); + expect(res.status).toBe(200); + expect(await res.json()).toEqual({ foo: 'bar', bar: 'baz' }); + }); });