basant307/AI_Governance_Project
048
1import type { Emitter } from 'strict-event-emitter'2import { DeferredPromise } from '@open-draft/deferred-promise'3import { until } from '@open-draft/until'4import type { HttpRequestEventMap } from '../glossary'5import { emitAsync } from './emitAsync'6import { kResponsePromise, RequestController } from '../RequestController'7import {8 createServerErrorResponse,9 isResponseError,10 isResponseLike,11 ResponseError,12} from './responseUtils'13import { InterceptorError } from '../InterceptorError'14import { isNodeLikeError } from './isNodeLikeError'15import { isObject } from './isObject'16 17interface HandleRequestOptions {18 requestId: string19 request: Request20 emitter: Emitter<HttpRequestEventMap>21 controller: RequestController22 23 /**24 * Called when the request has been handled25 * with the given `Response` instance.26 */27 onResponse: (response: Response) => void | Promise<void>28 29 /**30 * Called when the request has been handled31 * with the given `Response.error()` instance.32 */33 onRequestError: (response: ResponseError) => void34 35 /**36 * Called when an unhandled error happens during the37 * request handling. This is never a thrown error/response.38 */39 onError: (error: unknown) => void40}41 42/**43 * @returns {Promise<boolean>} Indicates whether the request has been handled.44 */45export async function handleRequest(46 options: HandleRequestOptions47): Promise<boolean> {48 const handleResponse = async (49 response: Response | Error | Record<string, any>50 ) => {51 if (response instanceof Error) {52 options.onError(response)53 return true54 }55 56 // Handle "Response.error()" instances.57 if (isResponseError(response)) {58 options.onRequestError(response)59 return true60 }61 62 /**63 * Handle normal responses or response-like objects.64 * @note This must come before the arbitrary object check65 * since Response instances are, in fact, objects.66 */67 if (isResponseLike(response)) {68 await options.onResponse(response)69 return true70 }71 72 // Handle arbitrary objects provided to `.errorWith(reason)`.73 if (isObject(response)) {74 options.onError(response)75 return true76 }77 78 return false79 }80 81 const handleResponseError = async (error: unknown): Promise<boolean> => {82 // Forward the special interceptor error instances83 // to the developer. These must not be handled in any way.84 if (error instanceof InterceptorError) {85 throw result.error86 }87 88 // Support mocking Node.js-like errors.89 if (isNodeLikeError(error)) {90 options.onError(error)91 return true92 }93 94 // Handle thrown responses.95 if (error instanceof Response) {96 return await handleResponse(error)97 }98 99 return false100 }101 102 // Add the last "request" listener to check if the request103 // has been handled in any way. If it hasn't, resolve the104 // response promise with undefined.105 options.emitter.once('request', ({ requestId: pendingRequestId }) => {106 if (pendingRequestId !== options.requestId) {107 return108 }109 110 if (options.controller[kResponsePromise].state === 'pending') {111 options.controller[kResponsePromise].resolve(undefined)112 }113 })114 115 const requestAbortPromise = new DeferredPromise<void, unknown>()116 117 /**118 * @note `signal` is not always defined in React Native.119 */120 if (options.request.signal) {121 if (options.request.signal.aborted) {122 requestAbortPromise.reject(options.request.signal.reason)123 } else {124 options.request.signal.addEventListener(125 'abort',126 () => {127 requestAbortPromise.reject(options.request.signal.reason)128 },129 { once: true }130 )131 }132 }133 134 const result = await until(async () => {135 // Emit the "request" event and wait until all the listeners136 // for that event are finished (e.g. async listeners awaited).137 // By the end of this promise, the developer cannot affect the138 // request anymore.139 const requestListenersPromise = emitAsync(options.emitter, 'request', {140 requestId: options.requestId,141 request: options.request,142 controller: options.controller,143 })144 145 await Promise.race([146 // Short-circuit the request handling promise if the request gets aborted.147 requestAbortPromise,148 requestListenersPromise,149 options.controller[kResponsePromise],150 ])151 152 // The response promise will settle immediately once153 // the developer calls either "respondWith" or "errorWith".154 return await options.controller[kResponsePromise]155 })156 157 // Handle the request being aborted while waiting for the request listeners.158 if (requestAbortPromise.state === 'rejected') {159 options.onError(requestAbortPromise.rejectionReason)160 return true161 }162 163 if (result.error) {164 // Handle the error during the request listener execution.165 // These can be thrown responses or request errors.166 if (await handleResponseError(result.error)) {167 return true168 }169 170 // If the developer has added "unhandledException" listeners,171 // allow them to handle the error. They can translate it to a172 // mocked response, network error, or forward it as-is.173 if (options.emitter.listenerCount('unhandledException') > 0) {174 // Create a new request controller just for the unhandled exception case.175 // This is needed because the original controller might have been already176 // interacted with (e.g. "respondWith" or "errorWith" called on it).177 const unhandledExceptionController = new RequestController(178 options.request179 )180 181 await emitAsync(options.emitter, 'unhandledException', {182 error: result.error,183 request: options.request,184 requestId: options.requestId,185 controller: unhandledExceptionController,186 }).then(() => {187 // If all the "unhandledException" listeners have finished188 // but have not handled the response in any way, preemptively189 // resolve the pending response promise from the new controller.190 // This prevents it from hanging forever.191 if (192 unhandledExceptionController[kResponsePromise].state === 'pending'193 ) {194 unhandledExceptionController[kResponsePromise].resolve(undefined)195 }196 })197 198 const nextResult = await until(199 () => unhandledExceptionController[kResponsePromise]200 )201 202 /**203 * @note Handle the result of the unhandled controller204 * in the same way as the original request controller.205 * The exception here is that thrown errors within the206 * "unhandledException" event do NOT result in another207 * emit of the same event. They are forwarded as-is.208 */209 if (nextResult.error) {210 return handleResponseError(nextResult.error)211 }212 213 if (nextResult.data) {214 return handleResponse(nextResult.data)215 }216 }217 218 // Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response.219 options.onResponse(createServerErrorResponse(result.error))220 return true221 }222 223 /**224 * Handle a mocked Response instance.225 * @note That this can also be an Error in case226 * the developer called "errorWith". This differentiates227 * unhandled exceptions from intended errors.228 */229 if (result.data) {230 return handleResponse(result.data)231 }232 233 // In all other cases, consider the request unhandled.234 return false235}236 