diff --git a/src/core/handlers/RequestHandler.ts b/src/core/handlers/RequestHandler.ts index 8369ec233..917d6edc4 100644 --- a/src/core/handlers/RequestHandler.ts +++ b/src/core/handlers/RequestHandler.ts @@ -156,7 +156,7 @@ export abstract class RequestHandler< resolutionContext?: ResponseResolutionContext }): Promise { const parsedResult = await this.parse({ - request: args.request.clone(), + request: args.request, resolutionContext: args.resolutionContext, }) @@ -186,6 +186,8 @@ export abstract class RequestHandler< return null } + // Clone the request instance before it's passed to the handler phases + // and the response resolver so we can always read it for logging. const mainRequestRef = args.request.clone() // Immediately mark the handler as used. @@ -194,11 +196,11 @@ export abstract class RequestHandler< this.isUsed = true const parsedResult = await this.parse({ - request: mainRequestRef.clone(), + request: args.request, resolutionContext: args.resolutionContext, }) const shouldInterceptRequest = this.predicate({ - request: mainRequestRef.clone(), + request: args.request, parsedResult, resolutionContext: args.resolutionContext, }) diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts index ebf2d7650..ebacb5c57 100644 --- a/src/node/SetupServerApi.ts +++ b/src/node/SetupServerApi.ts @@ -1,3 +1,4 @@ +import { setMaxListeners, defaultMaxListeners } from 'node:events' import { invariant } from 'outvariant' import { BatchInterceptor, @@ -50,6 +51,18 @@ export class SetupServerApi */ private init(): void { this.interceptor.on('request', async ({ request, requestId }) => { + // Bump the maximum number of event listeners on the + // request's "AbortSignal". This prepares the request + // for each request handler cloning it at least once. + // Note that cloning a request automatically appends a + // new "abort" event listener to the parent request's + // "AbortController" so if the parent aborts, all the + // clones are automatically aborted. + setMaxListeners( + Math.max(defaultMaxListeners, this.currentHandlers.length), + request.signal, + ) + const response = await handleRequest( request, requestId, diff --git a/test/node/msw-api/many-request-handlers.test.ts b/test/node/msw-api/many-request-handlers.test.ts new file mode 100644 index 000000000..ecdd4d793 --- /dev/null +++ b/test/node/msw-api/many-request-handlers.test.ts @@ -0,0 +1,58 @@ +/** + * @jest-environment node + */ +import { graphql, http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +// Create a large number of request handlers. +const restHandlers = new Array(100).fill(null).map((_, index) => { + return http.post( + `https://example.com/resource/${index}`, + async ({ request }) => { + const text = await request.text() + return HttpResponse.text(text + index.toString()) + }, + ) +}) + +const graphqlHanlers = new Array(100).fill(null).map((_, index) => { + return graphql.query(`Get${index}`, () => { + return HttpResponse.json({ data: { index } }) + }) +}) + +const server = setupServer(...restHandlers, ...graphqlHanlers) + +beforeAll(() => { + server.listen() + jest.spyOn(process.stderr, 'write') +}) + +afterAll(() => { + server.close() + jest.restoreAllMocks() +}) + +it('does not print a memory leak warning when having many request handlers', async () => { + const httpResponse = await fetch('https://example.com/resource/42', { + method: 'POST', + body: 'request-body-', + }).then((response) => response.text()) + + const graphqlResponse = await fetch('https://example.com', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `query Get42 { index }`, + }), + }).then((response) => response.json()) + + // Must not print any memory leak warnings. + expect(process.stderr.write).not.toHaveBeenCalled() + + // Must return the mocked response. + expect(httpResponse).toBe('request-body-42') + expect(graphqlResponse).toEqual({ data: { index: 42 } }) +})