UNPKG

get-it

Version:

Generic HTTP request library for node, browsers and workers

228 lines (190 loc) 6.01 kB
import type {HttpRequest, MiddlewareResponse, RequestOptions} from 'get-it' import parseHeaders from 'parse-headers' import type {RequestAdapter} from '../types' import {FetchXhr} from './browser/fetchXhr' /** * Use fetch if it's available, non-browser environments such as Deno, Edge Runtime and more provide fetch as a global but doesn't provide xhr * @public */ export const adapter = ( typeof XMLHttpRequest === 'function' ? ('xhr' as const) : ('fetch' as const) ) satisfies RequestAdapter // Fallback to fetch-based XHR polyfill for non-browser environments like Workers const XmlHttpRequest = adapter === 'xhr' ? XMLHttpRequest : FetchXhr export const httpRequester: HttpRequest = (context, callback) => { const opts = context.options const options = context.applyMiddleware('finalizeOptions', opts) as RequestOptions const timers: any = {} // Allow middleware to inject a response, for instance in the case of caching or mocking const injectedResponse = context.applyMiddleware('interceptRequest', undefined, { adapter, context, }) // If middleware injected a response, treat it as we normally would and return it // Do note that the injected response has to be reduced to a cross-environment friendly response if (injectedResponse) { const cbTimer = setTimeout(callback, 0, null, injectedResponse) const cancel = () => clearTimeout(cbTimer) return {abort: cancel} } // We'll want to null out the request on success/failure let xhr = new XmlHttpRequest() if (xhr instanceof FetchXhr && typeof options.fetch === 'object') { xhr.setInit(options.fetch, options.useAbortSignal ?? true) } const headers = options.headers const delays = options.timeout // Request state let aborted = false let loaded = false let timedOut = false // Apply event handlers xhr.onerror = (event: ProgressEvent) => { // If fetch is used then rethrow the original error if (xhr instanceof FetchXhr) { onError( event instanceof Error ? event : new Error(`Request error while attempting to reach is ${options.url}`, {cause: event}), ) } else { onError( new Error( `Request error while attempting to reach is ${options.url}${ event.lengthComputable ? `(${event.loaded} of ${event.total} bytes transferred)` : '' }`, ), ) } } xhr.ontimeout = (event: ProgressEvent) => { onError( new Error( `Request timeout while attempting to reach ${options.url}${ event.lengthComputable ? `(${event.loaded} of ${event.total} bytes transferred)` : '' }`, ), ) } xhr.onabort = () => { stopTimers(true) aborted = true } xhr.onreadystatechange = function () { // Prevent request from timing out resetTimers() if (aborted || !xhr || xhr.readyState !== 4) { return } // Will be handled by onError if (xhr.status === 0) { return } onLoad() } // @todo two last options to open() is username/password xhr.open( options.method!, options.url, true, // Always async ) // Some options need to be applied after open xhr.withCredentials = !!options.withCredentials // Set headers if (headers && xhr.setRequestHeader) { for (const key in headers) { // eslint-disable-next-line no-prototype-builtins if (headers.hasOwnProperty(key)) { xhr.setRequestHeader(key, headers[key]) } } } if (options.rawBody) { xhr.responseType = 'arraybuffer' } // Let middleware know we're about to do a request context.applyMiddleware('onRequest', {options, adapter, request: xhr, context}) xhr.send(options.body || null) // Figure out which timeouts to use (if any) if (delays) { timers.connect = setTimeout(() => timeoutRequest('ETIMEDOUT'), delays.connect) } return {abort} function abort() { aborted = true if (xhr) { xhr.abort() } } function timeoutRequest(code: any) { timedOut = true xhr.abort() const error: any = new Error( code === 'ESOCKETTIMEDOUT' ? `Socket timed out on request to ${options.url}` : `Connection timed out on request to ${options.url}`, ) error.code = code context.channels.error.publish(error) } function resetTimers() { if (!delays) { return } stopTimers() timers.socket = setTimeout(() => timeoutRequest('ESOCKETTIMEDOUT'), delays.socket) } function stopTimers(force?: boolean) { // Only clear the connect timeout if we've got a connection if (force || aborted || (xhr && xhr.readyState >= 2 && timers.connect)) { clearTimeout(timers.connect) } if (timers.socket) { clearTimeout(timers.socket) } } function onError(error: Error) { if (loaded) { return } // Clean up stopTimers(true) loaded = true ;(xhr as any) = null // Annoyingly, details are extremely scarce and hidden from us. // We only really know that it is a network error const err = (error || new Error(`Network error while attempting to reach ${options.url}`)) as Error & { isNetworkError: boolean request?: typeof options } err.isNetworkError = true err.request = options callback(err) } function reduceResponse(): MiddlewareResponse { return { body: xhr.response || (xhr.responseType === '' || xhr.responseType === 'text' ? xhr.responseText : ''), url: options.url, method: options.method!, headers: parseHeaders(xhr.getAllResponseHeaders()), statusCode: xhr.status!, statusMessage: xhr.statusText!, } } function onLoad() { if (aborted || loaded || timedOut) { return } if (xhr.status === 0) { onError(new Error('Unknown XHR error')) return } // Prevent being called twice stopTimers() loaded = true callback(null, reduceResponse()) } }