get-it
Version:
Generic HTTP request library for node, browsers and workers
163 lines (138 loc) • 5.09 kB
text/typescript
/* 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
}