Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve fetch fallback, drop dead IE legacy #64

Merged
merged 1 commit into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading