Skip to content

Commit

Permalink
feat: add "unhandledException" life-cycle event (#1199)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinJaskulla authored and kettanaito committed May 17, 2022
1 parent b6887a3 commit 5c90799
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/sharedOptions.ts
Expand Up @@ -21,6 +21,7 @@ export interface LifeCycleEventsMap<ResponseType> {
'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<ResponseType> = Pick<
Expand Down
20 changes: 15 additions & 5 deletions 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'
Expand Down Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions test/msw-api/setup-server/life-cycle-events/on.test.ts
Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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'))
Expand Down
9 changes: 9 additions & 0 deletions test/msw-api/setup-worker/life-cycle-events/on.mocks.ts
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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',
})
Expand Down
11 changes: 11 additions & 0 deletions test/msw-api/setup-worker/life-cycle-events/on.test.ts
Expand Up @@ -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()

Expand Down

0 comments on commit 5c90799

Please sign in to comment.