Skip to content

Commit

Permalink
fix(setupServer): suppress "ERR_INVALID_ARG_TYPE" (AbortSignal) error…
Browse files Browse the repository at this point in the history
…s from "setMaxListeners" in jsdom (#1779)

Co-authored-by: Artem Zakharchenko <kettanaito@gmail.com>
  • Loading branch information
christoph-fricke and kettanaito authored Oct 20, 2023
1 parent f5f31fa commit cb0a5cd
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 4 deletions.
27 changes: 23 additions & 4 deletions src/node/SetupServerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { mergeRight } from '~/core/utils/internal/mergeRight'
import { handleRequest } from '~/core/utils/handleRequest'
import { devUtils } from '~/core/utils/internal/devUtils'
import { SetupServer } from './glossary'
import { isNodeException } from './utils/isNodeException'

const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = {
onUnhandledRequest: 'warn',
Expand Down Expand Up @@ -62,10 +63,28 @@ export class SetupServerApi
// 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,
)
try {
setMaxListeners(
Math.max(defaultMaxListeners, this.currentHandlers.length),
request.signal,
)
} catch (error: unknown) {
/**
* @note Mock environments (JSDOM, ...) are not able to implement an internal
* "kIsNodeEventTarget" Symbol that Node.js uses to identify Node.js `EventTarget`s.
* `setMaxListeners` throws an error for non-Node.js `EventTarget`s.
* At the same time, mock environments are also not able to implement the
* internal "events.maxEventTargetListenersWarned" Symbol, which results in
* "MaxListenersExceededWarning" not being printed by Node.js for those anyway.
* The main reason for using `setMaxListeners` is to suppress these warnings in Node.js,
* which won't be printed anyway if `setMaxListeners` fails.
*/
if (
!(isNodeException(error) && error.code === 'ERR_INVALID_ARG_TYPE')
) {
throw error
}
}
}

const response = await handleRequest(
Expand Down
10 changes: 10 additions & 0 deletions src/node/utils/isNodeException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Determines if the given value is a Node.js exception.
* Node.js exceptions have additional information, like
* the `code` and `errno` properties.
*/
export function isNodeException(
error: unknown,
): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error
}
1 change: 1 addition & 0 deletions test/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
customExportConditions: [''],
},
globals: {
fetch,
Request,
Response,
TextEncoder,
Expand Down
65 changes: 65 additions & 0 deletions test/node/regressions/many-request-handlers-jsdom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @jest-environment jsdom
*
* @note In JSDOM, the "AbortSignal" class is polyfilled instead of
* using the Node.js global. Because of that, its instances won't
* pass the instance check of "require('event').setMaxListeners"
* (that's based on the internal Node.js symbol), resulting in
* an exception.
* @see /~https://github.com/mswjs/msw/pull/1779
*/
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 } })
})

0 comments on commit cb0a5cd

Please sign in to comment.