From 5c9079904202bf968c0f7c3c8da0fa68173ce7b4 Mon Sep 17 00:00:00 2001 From: Martin Jaskulla <35801656+MartinJaskulla@users.noreply.github.com> Date: Mon, 2 May 2022 12:47:02 +0200 Subject: [PATCH] feat: add "unhandledException" life-cycle event (#1199) --- src/sharedOptions.ts | 1 + src/utils/handleRequest.ts | 20 ++++++++++++++----- .../setup-server/life-cycle-events/on.test.ts | 19 ++++++++++++++++++ .../life-cycle-events/on.mocks.ts | 9 +++++++++ .../setup-worker/life-cycle-events/on.test.ts | 11 ++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/sharedOptions.ts b/src/sharedOptions.ts index 45546a523..b0e35f581 100644 --- a/src/sharedOptions.ts +++ b/src/sharedOptions.ts @@ -21,6 +21,7 @@ export interface LifeCycleEventsMap { 'request:end': (request: MockedRequest) => void 'response:mocked': (response: ResponseType, requestId: string) => void 'response:bypass': (response: ResponseType, requestId: string) => void + unhandledException: (error: Error, request: MockedRequest) => void } export type LifeCycleEventEmitter = Pick< diff --git a/src/utils/handleRequest.ts b/src/utils/handleRequest.ts index f8dd163b8..250f4309b 100644 --- a/src/utils/handleRequest.ts +++ b/src/utils/handleRequest.ts @@ -1,3 +1,4 @@ +import { until } from '@open-draft/until' import { StrictEventEmitter } from 'strict-event-emitter' import { MockedRequest, RequestHandler } from '../handlers/RequestHandler' import { ServerLifecycleEventsMap } from '../node/glossary' @@ -65,11 +66,20 @@ export async function handleRequest< } // Resolve a mocked response from the list of request handlers. - const lookupResult = await getResponse( - request, - handlers, - handleRequestOptions?.resolutionContext, - ) + const [lookupError, lookupResult] = await until(() => { + return getResponse( + request, + handlers, + handleRequestOptions?.resolutionContext, + ) + }) + + if (lookupError) { + // Allow developers to react to unhandled exceptions in request handlers. + emitter.emit('unhandledException', lookupError, request) + throw lookupError + } + const { handler, response } = lookupResult // When there's no handler for the request, consider it unhandled. diff --git a/test/msw-api/setup-server/life-cycle-events/on.test.ts b/test/msw-api/setup-server/life-cycle-events/on.test.ts index de6ea7f79..f00e39f5c 100644 --- a/test/msw-api/setup-server/life-cycle-events/on.test.ts +++ b/test/msw-api/setup-server/life-cycle-events/on.test.ts @@ -35,6 +35,9 @@ beforeAll(async () => { rest.post(httpServer.http.makeUrl('/no-response'), () => { return }), + rest.get(httpServer.http.makeUrl('/unhandled-exception'), () => { + throw new Error('Unhandled resolver error') + }), ) server.listen() @@ -62,6 +65,12 @@ beforeAll(async () => { listener(`[response:bypass] ${res.body} ${requestId}`) }) + server.events.on('unhandledException', (error, req) => { + listener( + `[unhandledException] ${req.method} ${req.url.href} ${req.id} ${error.message}`, + ) + }) + // Supress "Expected a mocking resolver function to return a mocked response" // warnings. Using intentional explicit empty resolver. jest.spyOn(global.console, 'warn').mockImplementation() @@ -156,6 +165,16 @@ test('emits events for an unhandled request', async () => { ) }) +test('emits unhandled exceptions in the request handler', async () => { + const url = httpServer.http.makeUrl('/unhandled-exception') + await fetch(url).catch(() => undefined) + const requestId = getRequestId(listener) + + expect(listener).toHaveBeenCalledWith( + `[unhandledException] GET ${url} ${requestId} Unhandled resolver error`, + ) +}) + test('stops emitting events once the server is stopped', async () => { server.close() await fetch(httpServer.http.makeUrl('/user')) diff --git a/test/msw-api/setup-worker/life-cycle-events/on.mocks.ts b/test/msw-api/setup-worker/life-cycle-events/on.mocks.ts index 19e5b0342..f32eac7f9 100644 --- a/test/msw-api/setup-worker/life-cycle-events/on.mocks.ts +++ b/test/msw-api/setup-worker/life-cycle-events/on.mocks.ts @@ -8,6 +8,9 @@ const worker = setupWorker( rest.post('/no-response', () => { return }), + rest.get('/unhandled-exception', () => { + throw new Error('Unhandled resolver error') + }), ) worker.events.on('request:start', (req) => { @@ -37,6 +40,12 @@ worker.events.on('response:bypass', async (res, requestId) => { console.warn(`[response:bypass] ${body} ${requestId}`) }) +worker.events.on('unhandledException', (error, req) => { + console.warn( + `[unhandledException] ${req.method} ${req.url.href} ${req.id} ${error.message}`, + ) +}) + worker.start({ onUnhandledRequest: 'bypass', }) diff --git a/test/msw-api/setup-worker/life-cycle-events/on.test.ts b/test/msw-api/setup-worker/life-cycle-events/on.test.ts index ae6ff7c4e..d7c5ce561 100644 --- a/test/msw-api/setup-worker/life-cycle-events/on.test.ts +++ b/test/msw-api/setup-worker/life-cycle-events/on.test.ts @@ -92,6 +92,17 @@ test('emits events for an unhandled request', async () => { ]) }) +test('emits unhandled exceptions in the request handler', async () => { + const runtime = await createRuntime() + const url = runtime.makeUrl('/unhandled-exception') + await runtime.request(url) + const requestId = getRequestId(runtime.consoleSpy) + + expect(runtime.consoleSpy.get('warning')).toContain( + `[unhandledException] GET ${url} ${requestId} Unhandled resolver error`, + ) +}) + test('stops emitting events once the worker is stopped', async () => { const runtime = await createRuntime()