UNPKG

get-it

Version:

Generic HTTP request library for node, browsers and workers

163 lines (138 loc) 5.09 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import {processOptions} from './middleware/defaultOptionsProcessor' import {validateOptions} from './middleware/defaultOptionsValidator' import type { HttpContext, HttpRequest, HttpRequestOngoing, Middleware, MiddlewareChannels, MiddlewareHooks, MiddlewareReducer, MiddlewareResponse, Middlewares, Requester, RequestOptions, } from './types' import {middlewareReducer} from './util/middlewareReducer' import {createPubSub} from './util/pubsub' const channelNames = [ 'request', 'response', 'progress', 'error', 'abort', ] satisfies (keyof MiddlewareChannels)[] const middlehooks = [ 'processOptions', 'validateOptions', 'interceptRequest', 'finalizeOptions', 'onRequest', 'onResponse', 'onError', 'onReturn', 'onHeaders', ] satisfies (keyof MiddlewareHooks)[] /** @public */ export function createRequester(initMiddleware: Middlewares, httpRequest: HttpRequest): Requester { const loadedMiddleware: Middlewares = [] const middleware: MiddlewareReducer = middlehooks.reduce( (ware, name) => { ware[name] = ware[name] || [] return ware }, { processOptions: [processOptions], validateOptions: [validateOptions], } as any, ) function request(opts: RequestOptions | string) { const onResponse = (reqErr: Error | null, res: MiddlewareResponse, ctx: HttpContext) => { let error = reqErr let response: MiddlewareResponse | null = res // We're processing non-errors first, in case a middleware converts the // response into an error (for instance, status >= 400 == HttpError) if (!error) { try { response = applyMiddleware('onResponse', res, ctx) } catch (err: any) { response = null error = err } } // Apply error middleware - if middleware return the same (or a different) error, // publish as an error event. If we *don't* return an error, assume it has been handled error = error && applyMiddleware('onError', error, ctx) // Figure out if we should publish on error/response channels if (error) { channels.error.publish(error) } else if (response) { channels.response.publish(response) } } const channels: MiddlewareChannels = channelNames.reduce((target, name) => { target[name] = createPubSub() as MiddlewareChannels[typeof name] return target }, {} as any) // Prepare a middleware reducer that can be reused throughout the lifecycle const applyMiddleware = middlewareReducer(middleware) // Parse the passed options const options = applyMiddleware('processOptions', opts as RequestOptions) // Validate the options applyMiddleware('validateOptions', options) // Build a context object we can pass to child handlers const context = {options, channels, applyMiddleware} // We need to hold a reference to the current, ongoing request, // in order to allow cancellation. In the case of the retry middleware, // a new request might be triggered let ongoingRequest: HttpRequestOngoing | undefined const unsubscribe = channels.request.subscribe((ctx) => { // Let request adapters (node/browser) perform the actual request ongoingRequest = httpRequest(ctx, (err, res) => onResponse(err, res!, ctx)) }) // If we abort the request, prevent further requests from happening, // and be sure to cancel any ongoing request (obviously) channels.abort.subscribe(() => { unsubscribe() if (ongoingRequest) { ongoingRequest.abort() } }) // See if any middleware wants to modify the return value - for instance // the promise or observable middlewares const returnValue = applyMiddleware('onReturn', channels, context) // If return value has been modified by a middleware, we expect the middleware // to publish on the 'request' channel. If it hasn't been modified, we want to // trigger it right away if (returnValue === channels) { channels.request.publish(context) } return returnValue } request.use = function use(newMiddleware: Middleware) { if (!newMiddleware) { throw new Error('Tried to add middleware that resolved to falsey value') } if (typeof newMiddleware === 'function') { throw new Error( 'Tried to add middleware that was a function. It probably expects you to pass options to it.', ) } if (newMiddleware.onReturn && middleware.onReturn.length > 0) { throw new Error( 'Tried to add new middleware with `onReturn` handler, but another handler has already been registered for this event', ) } middlehooks.forEach((key) => { if (newMiddleware[key]) { middleware[key].push(newMiddleware[key] as any) } }) loadedMiddleware.push(newMiddleware) return request } request.clone = () => createRequester(loadedMiddleware, httpRequest) initMiddleware.forEach(request.use) return request }