UNPKG

get-it

Version:

Generic HTTP request library for node, browsers and workers

115 lines (110 loc) 3.91 kB
/** * Mimicks the XMLHttpRequest API with only the parts needed for get-it's XHR adapter */ export class FetchXhr implements Pick<XMLHttpRequest, 'open' | 'abort' | 'getAllResponseHeaders' | 'setRequestHeader'> { /** * Public interface, interop with real XMLHttpRequest */ onabort: (() => void) | undefined onerror: ((error?: any) => void) | undefined onreadystatechange: (() => void) | undefined ontimeout: XMLHttpRequest['ontimeout'] | undefined /** * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState */ readyState: 0 | 1 | 2 | 3 | 4 = 0 response: XMLHttpRequest['response'] responseText: XMLHttpRequest['responseText'] = '' responseType: XMLHttpRequest['responseType'] = '' status: XMLHttpRequest['status'] | undefined statusText: XMLHttpRequest['statusText'] | undefined withCredentials: XMLHttpRequest['withCredentials'] | undefined /** * Private implementation details */ #method!: string #url!: string #resHeaders!: string #headers: Record<string, string> = {} #controller?: AbortController #init: RequestInit = {} #useAbortSignal?: boolean // eslint-disable-next-line @typescript-eslint/no-unused-vars -- _async is only declared for typings compatibility open(method: string, url: string, _async?: boolean) { this.#method = method this.#url = url this.#resHeaders = '' this.readyState = 1 // Open this.onreadystatechange?.() this.#controller = undefined } abort() { if (this.#controller) { this.#controller.abort() } } getAllResponseHeaders() { return this.#resHeaders } setRequestHeader(name: string, value: string) { this.#headers[name] = value } // Allow setting extra fetch init options, needed for runtimes such as Vercel Edge to set `cache` and other options in React Server Components setInit(init: RequestInit, useAbortSignal = true) { this.#init = init this.#useAbortSignal = useAbortSignal } send(body: BodyInit) { const textBody = this.responseType !== 'arraybuffer' const options: RequestInit = { ...this.#init, method: this.#method, headers: this.#headers, body, } if (typeof AbortController === 'function' && this.#useAbortSignal) { this.#controller = new AbortController() // The instanceof check ensures environments like Edge Runtime, Node 18 with built-in fetch // and more don't throw if `signal` doesn't implement`EventTarget` // Native browser AbortSignal implements EventTarget, so we can use it if (typeof EventTarget !== 'undefined' && this.#controller.signal instanceof EventTarget) { options.signal = this.#controller.signal } } // Some environments (like CloudFlare workers) don't support credentials in // RequestInitDict, and there doesn't seem to be any easy way to check for it, // so for now let's just make do with a document check :/ if (typeof document !== 'undefined') { options.credentials = this.withCredentials ? 'include' : 'omit' } fetch(this.#url, options) .then((res): Promise<string | ArrayBuffer> => { res.headers.forEach((value: any, key: any) => { this.#resHeaders += `${key}: ${value}\r\n` }) this.status = res.status this.statusText = res.statusText this.readyState = 3 // Loading this.onreadystatechange?.() return textBody ? res.text() : res.arrayBuffer() }) .then((resBody) => { if (typeof resBody === 'string') { this.responseText = resBody } else { this.response = resBody } this.readyState = 4 // Done this.onreadystatechange?.() }) .catch((err: Error) => { if (err.name === 'AbortError') { this.onabort?.() return } this.onerror?.(err) }) } }