developit / developit/unfetch

The second alternative to AbortController support (#54, #68, #137)

Open
#148 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
5.7k
Forks
204
PR merge metrics
No merged PRs in 30d

Description

Resolves #54
Alternative to #68, and #137

### This solution
- Supports single `abort()` for the several parallel fetch requests
- Actually do http request abort over the network (not wiaitng for the result and ignores it like with `Promise.race` surrogates)
- Based on standard `AbortController` implementation (can run with polyfills on IE, see below)
- Do not run request if it was aborted already
- Do not abort request if it was complete (`readyState === DONE_STATE`)
- Add credentials not only for `include` but for `same-origin` also (usefull for some CORS use cases)
- Accumulates #54, #68, #137 discussions, [this](https://github.com/stereobooster/react-ideal-image/blob/master/src/components/unfetch.js) `unfetch` enchancement and [this](https://codesandbox.io/s/92vnjok08r?file=/src/xhr/xhr.js:316-330) implementation alternative

Not sure about **500 bytes** of the bundle size, but should be very close to it :wink:

### The code (`abortable-unfetch.mjs`)
```js
export default function (url, { method, headers, credentials, body, signal } = {}) {
return new Promise((resolve, reject) => {
const abortError = () => {
try {
return new DOMException('Aborted', 'AbortError')
} catch (error) { /* the DOMException constructor is not supported */
const abortError = new Error('Aborted')
abortError.name = 'AbortError'
return abortError
}
}

if (signal && signal.aborted) {
reject(abortError())
return
}

const request = new XMLHttpRequest()
const keys = []
const all = []
const respHeaders = {}

const response = () => ({
ok: (request.status / 100 | 0) === 2, // 200-299
statusText: request.statusText,
status: request.status,
url: request.responseURL,
text: () => Promise.resolve(request.responseText),
json: () => Promise.resolve(request.responseText).then(JSON.parse),
blob: () => Promise.resolve(new Blob([request.response])),
clone: response,
headers: {
keys: () => keys,
entries: () => all,
get: n => respHeaders[n.toLowerCase()],
has: n => n.toLowerCase() in respHeaders,
},
})

request.open(method || 'get', url, true)

request.onload = () => {
request.getAllResponseHeaders().
replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm,
(m, key, value) => {
keys.push(key = key.toLowerCase())
all.push([key, value])
respHeaders[key] = respHeaders[key]
? `${respHeaders[key]},${value}`
: value
})
resolve(response())
}

if (signal) {
const abortListener = () => request.abort()
signal.addEventListener('abort', abortListener)
request.onreadystatechange = () => {
if (request.readyState === 4) { /* DONE_STATE = 4 */
signal.removeEventListener('abort', abortListener)
}
}
}
request.onabort = () => reject(abortError())
request.onerror = reject

request.withCredentials = credentials === 'same-origin' || credentials === 'include'

for (const i in headers) {
request.setRequestHeader(i, headers[i])
}

request.send(body || null)
})
}
```

### The polyfill (`abortable-unfetch-polyfill.mjs`)
```js
import abortableUnfetch from './abortable-unfetch'

const g =
typeof self !== 'undefined' ? self :
typeof window !== 'undefined' ? window :
typeof global !== 'undefined' ? global :
undefined

if (g) {
if (typeof g.fetch === 'undefined') {
g.fetch = abortableUnfetch
}
}
```

### `AbortController` polyfills

There are two alternatives to polyfill the standard `AbortController` behaviour on the old browsers (like IE11):
- [`abortcontroller-polyfill`](https://github.com/mo/abortcontroller-polyfill)
- [`abort-controller`](https://github.com/mysticatea/abort-controller#readme)

For the minimum bundle size I prefer the first one and because we based on the standard AbortController API we do not need to polyfill fetch additionally:
```sh
npm i abortcontroller-polyfill
yarn add abortcontroller-polyfill
pnpm add abortcontroller-polyfill
```
Then somewhere in your `index.mjs`:
```js
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
import './abortable-unfetch-polyfill'
```
Do not forget to exclude transpiled polyfill code from the Babel transpile:
`exclude: [ "node_modules/**" ],` or `exclude: [ "node_modules/abortcontroller-polyfill/**" ],`

### Usage example
```js
let abortController

// abort can be called as many times as you want (it run only ones)
const abort = () => abortController && abortController.abort()

const doFetch = () => {

abort() // abort the previous / ongoing call
abortController = new AbortController()

fetch('http://api.plos.org/search?q=title:DNA',
{ credentials: 'same-origin', signal: abortController.signal }).
then(response => {
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`)
}
return response.json()
}).
then(data => {
console.log('REQUEST FINISHED')
console.dir(data)
}).
catch(error => {
if (error.name === 'AbortError') {
console.log('REQUEST ABORTED')
} else {
console.error(`REQUEST FAILED: ${error ? error.message : ''}`)
}

/* The alternative way to distinct the AbortError */
if (abortController.signal.aborted) {
console.log('REQUEST ABORTED')
} else {
console.error(`REQUEST FAILED: ${error ? error.message : ''}`)
}
})
}

doFetch()
setTimeout(abort, 5000)
```

P.S. @developit, @prk3, @simonbuerger, @prabirshrestha PR, tests and discussion are welcome :smile:

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.