UNPKG

@mswjs/interceptors

Version:

Low-level HTTP/HTTPS/XHR/fetch request interception library.

206 lines (181 loc) 6.62 kB
import type { Emitter } from 'strict-event-emitter' import { DeferredPromise } from '@open-draft/deferred-promise' import { until } from '@open-draft/until' import type { HttpRequestEventMap } from '../glossary' import { emitAsync } from './emitAsync' import { RequestController } from '../RequestController' import { createServerErrorResponse, isResponseError, isResponseLike, } from './responseUtils' import { InterceptorError } from '../InterceptorError' import { isNodeLikeError } from './isNodeLikeError' import { isObject } from './isObject' interface HandleRequestOptions { requestId: string request: Request emitter: Emitter<HttpRequestEventMap> controller: RequestController } export async function handleRequest( options: HandleRequestOptions ): Promise<void> { const handleResponse = async ( response: Response | Error | Record<string, any> ) => { if (response instanceof Error) { await options.controller.errorWith(response) return true } // Handle "Response.error()" instances. if (isResponseError(response)) { await options.controller.respondWith(response) return true } /** * Handle normal responses or response-like objects. * @note This must come before the arbitrary object check * since Response instances are, in fact, objects. */ if (isResponseLike(response)) { await options.controller.respondWith(response) return true } // Handle arbitrary objects provided to `.errorWith(reason)`. if (isObject(response)) { await options.controller.errorWith(response) return true } return false } const handleResponseError = async (error: unknown): Promise<boolean> => { // Forward the special interceptor error instances // to the developer. These must not be handled in any way. if (error instanceof InterceptorError) { throw result.error } // Support mocking Node.js-like errors. if (isNodeLikeError(error)) { await options.controller.errorWith(error) return true } // Handle thrown responses. if (error instanceof Response) { return await handleResponse(error) } return false } // Add the last "request" listener to check if the request // has been handled in any way. If it hasn't, resolve the // response promise with undefined. // options.emitter.once('request', async ({ requestId: pendingRequestId }) => { // if ( // pendingRequestId === options.requestId && // options.controller.readyState === RequestController.PENDING // ) { // await options.controller.passthrough() // } // }) const requestAbortPromise = new DeferredPromise<void, unknown>() /** * @note `signal` is not always defined in React Native. */ if (options.request.signal) { if (options.request.signal.aborted) { await options.controller.errorWith(options.request.signal.reason) return } options.request.signal.addEventListener( 'abort', () => { requestAbortPromise.reject(options.request.signal.reason) }, { once: true } ) } const result = await until(async () => { // Emit the "request" event and wait until all the listeners // for that event are finished (e.g. async listeners awaited). // By the end of this promise, the developer cannot affect the // request anymore. const requestListenersPromise = emitAsync(options.emitter, 'request', { requestId: options.requestId, request: options.request, controller: options.controller, }) await Promise.race([ // Short-circuit the request handling promise if the request gets aborted. requestAbortPromise, requestListenersPromise, options.controller.handled, ]) }) // Handle the request being aborted while waiting for the request listeners. if (requestAbortPromise.state === 'rejected') { await options.controller.errorWith(requestAbortPromise.rejectionReason) return } if (result.error) { // Handle the error during the request listener execution. // These can be thrown responses or request errors. if (await handleResponseError(result.error)) { return } // If the developer has added "unhandledException" listeners, // allow them to handle the error. They can translate it to a // mocked response, network error, or forward it as-is. if (options.emitter.listenerCount('unhandledException') > 0) { // Create a new request controller just for the unhandled exception case. // This is needed because the original controller might have been already // interacted with (e.g. "respondWith" or "errorWith" called on it). const unhandledExceptionController = new RequestController( options.request, { /** * @note Intentionally empty passthrough handle. * This controller is created within another controller and we only need * to know if `unhandledException` listeners handled the request. */ passthrough() {}, async respondWith(response) { await handleResponse(response) }, async errorWith(reason) { /** * @note Handle the result of the unhandled controller * in the same way as the original request controller. * The exception here is that thrown errors within the * "unhandledException" event do NOT result in another * emit of the same event. They are forwarded as-is. */ await options.controller.errorWith(reason) }, } ) await emitAsync(options.emitter, 'unhandledException', { error: result.error, request: options.request, requestId: options.requestId, controller: unhandledExceptionController, }) // If all the "unhandledException" listeners have finished // but have not handled the request in any way, passthrough. if ( unhandledExceptionController.readyState !== RequestController.PENDING ) { return } } // Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response. await options.controller.respondWith( createServerErrorResponse(result.error) ) return } // If the request hasn't been handled by this point, passthrough. if (options.controller.readyState === RequestController.PENDING) { return await options.controller.passthrough() } return options.controller.handled }