diff --git a/src/core/handlers/RemoteRequestHandler.ts b/src/core/handlers/RemoteRequestHandler.ts index 76787a5b0..b7eff4fab 100644 --- a/src/core/handlers/RemoteRequestHandler.ts +++ b/src/core/handlers/RemoteRequestHandler.ts @@ -5,7 +5,7 @@ import { type ResponseResolver, type RequestHandlerDefaultInfo, } from './RequestHandler' -import { RemoteClient } from 'node/setupRemoteServer' +import { RemoteClient } from '../../node/setupRemoteServer' interface RemoteRequestHandlerParsedResult { response: Response | undefined diff --git a/src/node/setupRemoteServer.ts b/src/node/setupRemoteServer.ts index d86ead0ed..c53f80fe3 100644 --- a/src/node/setupRemoteServer.ts +++ b/src/node/setupRemoteServer.ts @@ -2,6 +2,7 @@ import * as http from 'node:http' import { Readable } from 'node:stream' import * as streamConsumers from 'node:stream/consumers' import { AsyncLocalStorage } from 'node:async_hooks' +import type { RequiredDeep } from 'type-fest' import { invariant } from 'outvariant' import { createRequestId, FetchResponse } from '@mswjs/interceptors' import { DeferredPromise } from '@open-draft/deferred-promise' @@ -19,6 +20,10 @@ import type { } from '~/core/sharedOptions' import { devUtils } from '~/core/utils/internal/devUtils' import { AsyncHandlersController } from './SetupServerApi' +import { ListenOptions } from './glossary' +import { mergeRight } from '~/core/utils/internal/mergeRight' +import { DEFAULT_LISTEN_OPTIONS } from './SetupServerCommonApi' +import { onUnhandledRequest } from '~/core/utils/request/onUnhandledRequest' interface RemoteServerBoundaryContext { serverUrl: URL @@ -90,6 +95,7 @@ export class SetupRemoteServerApi { [kServerUrl]: URL | undefined + protected resolvedOptions!: RequiredDeep protected executionContexts: Map RemoteServerBoundaryContext> constructor(handlers: Array) { @@ -123,7 +129,11 @@ export class SetupRemoteServerApi return context.boundaryId } - public async listen(): Promise { + public async listen(options: Partial = {}): Promise { + this.resolvedOptions = mergeRight( + DEFAULT_LISTEN_OPTIONS, + options, + ) as RequiredDeep const dummyEmitter = new Emitter() const server = await createSyncServer() @@ -203,8 +213,15 @@ export class SetupRemoteServerApi request, requestId, handlers, - /** @todo Support listen options */ - { onUnhandledRequest() {} }, + { + /** + * @note Ignore the `onUnhandledRequest` callback during the + * request handling. This context isn't the only one handling + * the request. Instead, this logic is moved to the forwarded + * life-cycle event. + */ + onUnhandledRequest() {}, + }, /** * @note Use a dummy emitter because this context * is only one layer that can resolve a request. For example, @@ -231,6 +248,15 @@ export class SetupRemoteServerApi outgoing.writeHead(404).end() }) + + this.emitter.on('request:unhandled', async ({ request }) => { + /** + * @note React to unhandled requests in the "request:unhandled" listener. + * This event will be forwarded from the remote process after neither has + * handled the request. + */ + await onUnhandledRequest(request, this.resolvedOptions.onUnhandledRequest) + }) } public boundary, R>( diff --git a/test/node/msw-api/setup-remote-server/on-unhandled-request-default.test.ts b/test/node/msw-api/setup-remote-server/on-unhandled-request-default.test.ts new file mode 100644 index 000000000..96eb940e6 --- /dev/null +++ b/test/node/msw-api/setup-remote-server/on-unhandled-request-default.test.ts @@ -0,0 +1,47 @@ +// @vitest-environment node +import { HttpResponse, http } from 'msw' +import { setupRemoteServer } from 'msw/node' +import { spawnTestApp } from './utils' + +const remote = setupRemoteServer() + +beforeAll(async () => { + vi.spyOn(console, 'warn').mockImplementation(() => {}) + await remote.listen() +}) + +afterEach(() => { + vi.clearAllMocks() + remote.resetHandlers() +}) + +afterAll(async () => { + vi.restoreAllMocks() + await remote.close() +}) + +it( + 'warns on requests not handled by either party be default', + remote.boundary(async () => { + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + await fetch(new URL('/resource', testApp.url)) + + // Must print a warning since nobody has handled the request. + expect(console.warn).toHaveBeenCalledWith('') + }), +) + +it( + 'does not warn on the request not handled here but handled there', + remote.boundary(async () => { + throw new Error('Complete this') + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + await fetch(new URL('/resource', testApp.url)) + + // Must print a warning since nobody has handled the request. + expect(console.warn).toHaveBeenCalledWith('') + }), +) diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts index 30e4f0a5d..eb531f8da 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/default.node.test.ts @@ -17,7 +17,7 @@ beforeAll(() => { }) afterEach(() => { - vi.resetAllMocks() + vi.clearAllMocks() }) afterAll(() => { @@ -41,11 +41,4 @@ If you still wish to intercept this unhandled request, please create a request h Read more: https://mswjs.io/docs/getting-started/mocks`) }) -it('does not warn on unhandled "file://" requests', async () => { - // This request is expected to fail: - // Fetching non-existing file URL. - await fetch('file:///file/does/not/exist').catch(() => void 0) - - expect(console.error).not.toBeCalled() - expect(console.warn).not.toBeCalled() -}) +it.todo('does not warn on unhandled "file://" requests')