sam522's picture
node
d4b85c0
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 { kResponsePromise, RequestController } from '../RequestController'
import {
createServerErrorResponse,
isResponseError,
ResponseError,
} from './responseUtils'
import { InterceptorError } from '../InterceptorError'
import { isNodeLikeError } from './isNodeLikeError'
interface HandleRequestOptions {
requestId: string
request: Request
emitter: Emitter<HttpRequestEventMap>
controller: RequestController
/**
* Called when the request has been handled
* with the given `Response` instance.
*/
onResponse: (response: Response) => void | Promise<void>
/**
* Called when the request has been handled
* with the given `Response.error()` instance.
*/
onRequestError: (response: ResponseError) => void
/**
* Called when an unhandled error happens during the
* request handling. This is never a thrown error/response.
*/
onError: (error: unknown) => void
}
/**
* @returns {Promise<boolean>} Indicates whether the request has been handled.
*/
export async function handleRequest(
options: HandleRequestOptions
): Promise<boolean> {
const handleResponse = async (response: Response | Error) => {
if (response instanceof Error) {
options.onError(response)
}
// Handle "Response.error()" instances.
else if (isResponseError(response)) {
options.onRequestError(response)
} else {
await options.onResponse(response)
}
return true
}
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)) {
options.onError(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', ({ requestId: pendingRequestId }) => {
if (pendingRequestId !== options.requestId) {
return
}
if (options.controller[kResponsePromise].state === 'pending') {
options.controller[kResponsePromise].resolve(undefined)
}
})
const requestAbortPromise = new DeferredPromise<void, unknown>()
/**
* @note `signal` is not always defined in React Native.
*/
if (options.request.signal) {
if (options.request.signal.aborted) {
requestAbortPromise.reject(options.request.signal.reason)
} else {
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 requestListtenersPromise = 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,
requestListtenersPromise,
options.controller[kResponsePromise],
])
// The response promise will settle immediately once
// the developer calls either "respondWith" or "errorWith".
const mockedResponse = await options.controller[kResponsePromise]
return mockedResponse
})
// Handle the request being aborted while waiting for the request listeners.
if (requestAbortPromise.state === 'rejected') {
options.onError(requestAbortPromise.rejectionReason)
return true
}
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 true
}
// 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
)
await emitAsync(options.emitter, 'unhandledException', {
error: result.error,
request: options.request,
requestId: options.requestId,
controller: unhandledExceptionController,
}).then(() => {
// If all the "unhandledException" listeners have finished
// but have not handled the response in any way, preemptively
// resolve the pending response promise from the new controller.
// This prevents it from hanging forever.
if (
unhandledExceptionController[kResponsePromise].state === 'pending'
) {
unhandledExceptionController[kResponsePromise].resolve(undefined)
}
})
const nextResult = await until(
() => unhandledExceptionController[kResponsePromise]
)
/**
* @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.
*/
if (nextResult.error) {
return handleResponseError(nextResult.error)
}
if (nextResult.data) {
return handleResponse(nextResult.data)
}
}
// Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response.
options.onResponse(createServerErrorResponse(result.error))
return true
}
/**
* Handle a mocked Response instance.
* @note That this can also be an Error in case
* the developer called "errorWith". This differentiates
* unhandled exceptions from intended errors.
*/
if (result.data) {
return handleResponse(result.data)
}
// In all other cases, consider the request unhandled.
// The interceptor must perform it as-is.
return false
}