Skip to content

Commit

Permalink
fix: improve fetch fallback, drop dead IE legacy (#64)
Browse files Browse the repository at this point in the history
Co-authored-by: Cody Olsen <81981+stipsan@users.noreply.github.com>
  • Loading branch information
stipsan and stipsan committed Jan 10, 2023
1 parent f740021 commit 8fe6734
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 150 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -54,6 +54,12 @@ jobs:
- os: ubuntu-latest
# Test the actively developed version that will become the latest LTS release next October
node: current
# The `build` job already runs the testing suite in ubuntu and lts/*
exclude:
- os: ubuntu-latest
# Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life
node: lts/*

steps:
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3
Expand Down
4 changes: 0 additions & 4 deletions modules.d.ts
Expand Up @@ -5,10 +5,6 @@ declare module 'tunnel-agent' {
export function httpsOverHttps(options: any): any
}

declare module 'same-origin' {
export default function sameOrigin(uri1: string, uri2: string, ieMode?: boolean): boolean
}

declare module 'create-error-class' {
interface ErrorClass {
new (res: any, ctx: any): Error
Expand Down
16 changes: 2 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "get-it",
"version": "8.0.3",
"version": "8.0.4-fetch.0",
"description": "Generic HTTP request library for node, browsers and workers",
"keywords": [
"request",
Expand Down Expand Up @@ -105,10 +105,8 @@
"is-plain-object": "^5.0.0",
"is-retry-allowed": "^2.2.0",
"is-stream": "^2.0.1",
"nano-pubsub": "^2.0.1",
"parse-headers": "^2.0.5",
"progress-stream": "^2.0.0",
"same-origin": "^0.1.1",
"tunnel-agent": "^0.6.0",
"url-parse": "^1.5.10"
},
Expand Down
3 changes: 1 addition & 2 deletions src/createRequester.ts
@@ -1,9 +1,8 @@
import pubsub from 'nano-pubsub'

import {processOptions} from './middleware/defaultOptionsProcessor'
import {validateOptions} from './middleware/defaultOptionsValidator'
import type {HttpRequest, Middleware, Middlewares, Requester} from './types'
import middlewareReducer from './util/middlewareReducer'
import pubsub from './util/pubsub'

const channelNames = ['request', 'response', 'progress', 'error', 'abort']
const middlehooks = [
Expand Down
66 changes: 11 additions & 55 deletions src/request/browser-request.ts
@@ -1,37 +1,18 @@
import parseHeaders from 'parse-headers'
import sameOrigin from 'same-origin'

import FetchXhr from './browser/fetchXhr'
import {FetchXhr} from './browser/fetchXhr'

const noop = function () {
/* intentional noop */
}

// eslint-disable-next-line no-var
declare var XDomainRequest: any

const win = typeof document === 'undefined' || typeof window === 'undefined' ? undefined : window
const adapter = win ? 'xhr' : 'fetch'

let XmlHttpRequest: any = typeof XMLHttpRequest === 'function' ? XMLHttpRequest : noop
const hasXhr2 = 'withCredentials' in new XmlHttpRequest()
const XDR = typeof XDomainRequest === 'undefined' ? undefined : XDomainRequest
let CrossDomainRequest = hasXhr2 ? XmlHttpRequest : XDR
// 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
const adapter = typeof XMLHttpRequest === 'function' ? 'xhr' : 'fetch'

// Fallback to fetch-based XHR polyfill for non-browser environments like Workers
if (!win) {
XmlHttpRequest = FetchXhr
CrossDomainRequest = FetchXhr
}
const XmlHttpRequest = adapter === 'xhr' ? XMLHttpRequest : FetchXhr

export default (context: any, callback: any) => {
const opts = context.options
const options = context.applyMiddleware('finalizeOptions', opts)
const timers: any = {}

// Deep-checking window.location because of react native, where `location` doesn't exist
const cors = win && win.location && !sameOrigin(win.location.href, options.url)

// Allow middleware to inject a response, for instance in the case of caching or mocking
const injectedResponse = context.applyMiddleware('interceptRequest', undefined, {
adapter,
Expand All @@ -47,9 +28,8 @@ export default (context: any, callback: any) => {
}

// We'll want to null out the request on success/failure
let xhr = cors ? new CrossDomainRequest() : new XmlHttpRequest()
let xhr = new XmlHttpRequest()

const isXdr = win && (win as any).XDomainRequest && xhr instanceof (win as any).XDomainRequest
const headers = options.headers
const delays = options.timeout

Expand All @@ -66,17 +46,11 @@ export default (context: any, callback: any) => {
aborted = true
}

// IE9 must have onprogress be set to a unique function
xhr.onprogress = () => {
/* intentional noop */
}

const loadEvent = isXdr ? 'onload' : 'onreadystatechange'
xhr[loadEvent] = () => {
xhr.onreadystatechange = () => {
// Prevent request from timing out
resetTimers()

if (aborted || (xhr.readyState !== 4 && !isXdr)) {
if (aborted || xhr.readyState !== 4) {
return
}

Expand Down Expand Up @@ -106,8 +80,6 @@ export default (context: any, callback: any) => {
xhr.setRequestHeader(key, headers[key])
}
}
} else if (headers && isXdr) {
throw new Error('Headers cannot be set on an XDomainRequest object')
}

if (options.rawBody) {
Expand Down Expand Up @@ -174,7 +146,7 @@ export default (context: any, callback: any) => {
// Clean up
stopTimers(true)
loaded = true
xhr = null
;(xhr as any) = null

// Annoyingly, details are extremely scarce and hidden from us.
// We only really know that it is a network error
Expand All @@ -185,29 +157,13 @@ export default (context: any, callback: any) => {
}

function reduceResponse() {
let statusCode = xhr.status
let statusMessage = xhr.statusText

if (isXdr && statusCode === undefined) {
// IE8 CORS GET successful response doesn't have a status field, but body is fine
statusCode = 200
} else if (statusCode > 12000 && statusCode < 12156) {
// Yet another IE quirk where it emits weird status codes on network errors
// https://support.microsoft.com/en-us/kb/193625
return onError()
} else {
// Another IE bug where HTTP 204 somehow ends up as 1223
statusCode = xhr.status === 1223 ? 204 : xhr.status
statusMessage = xhr.status === 1223 ? 'No Content' : statusMessage
}

return {
body: xhr.response || xhr.responseText,
url: options.url,
method: options.method,
headers: isXdr ? {} : parseHeaders(xhr.getAllResponseHeaders()),
statusCode: statusCode,
statusMessage: statusMessage,
headers: parseHeaders(xhr.getAllResponseHeaders()),
statusCode: xhr.status,
statusMessage: xhr.statusText,
}
}

Expand Down
153 changes: 91 additions & 62 deletions src/request/browser/fetchXhr.ts
@@ -1,72 +1,101 @@
/**
* Mimicks the XMLHttpRequest API with only the parts needed for get-it's XHR adapter
*/
function FetchXhr(this: any) {
this.readyState = 0 // Unsent
}
FetchXhr.prototype.open = function (method: any, url: any) {
this._method = method
this._url = url
this._resHeaders = ''
this.readyState = 1 // Open
this.onreadystatechange()
}
FetchXhr.prototype.abort = function () {
if (this._controller) {
this._controller.abort()
export class FetchXhr
implements Pick<XMLHttpRequest, 'open' | 'abort' | 'getAllResponseHeaders' | 'setRequestHeader'>
{
/**
* Public interface, interop with real XMLHttpRequest
*/
onabort: () => void
onerror: (error?: any) => void
onreadystatechange: () => void
ontimeout: XMLHttpRequest['ontimeout']
/**
* 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']
statusText: XMLHttpRequest['statusText']
withCredentials: XMLHttpRequest['withCredentials']

/**
* Private implementation details
*/
#method: string
#url: string
#resHeaders: string
#headers: Record<string, string> = {}
#controller?: AbortController
// 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
}
}
FetchXhr.prototype.getAllResponseHeaders = function () {
return this._resHeaders
}
FetchXhr.prototype.setRequestHeader = function (key: any, value: any) {
this._headers = this._headers || {}
this._headers[key] = value
}
FetchXhr.prototype.send = function (body: any) {
const ctrl = (this._controller = typeof AbortController === 'function' && new AbortController())
const textBody = this.responseType !== 'arraybuffer'
const options: any = {
method: this._method,
headers: this._headers,
signal: (ctrl && ctrl.signal) || undefined,
body,
abort() {
if (this.#controller) {
this.#controller.abort()
}
}

// 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 window check :/
if (typeof document !== 'undefined') {
options.credentials = this.withCredentials ? 'include' : 'omit'
getAllResponseHeaders() {
return this.#resHeaders
}
setRequestHeader(name: string, value: string) {
this.#headers[name] = value
}
send(body: BodyInit) {
const textBody = this.responseType !== 'arraybuffer'
const options: RequestInit = {
method: this.#method,
headers: this.#headers,
signal: null,
body,
}
if (typeof AbortController === 'function') {
this.#controller = new AbortController()
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: any) => {
res.headers.forEach((value: any, key: any) => {
this._resHeaders += `${key}: ${value}\r\n`
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
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()
})
this.status = res.status
this.statusText = res.statusText
this.readyState = 3 // Loading
return textBody ? res.text() : res.arrayBuffer()
})
.then((resBody) => {
if (textBody) {
this.responseText = resBody
} else {
this.response = resBody
}
this.readyState = 4 // Done
this.onreadystatechange()
})
.catch((err) => {
if (err.name === 'AbortError') {
this.onabort()
return
}
.catch((err: Error) => {
if (err.name === 'AbortError') {
this.onabort()
return
}

this.onerror(err)
})
this.onerror?.(err)
})
}
}

export default FetchXhr

0 comments on commit 8fe6734

Please sign in to comment.