Open
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 forsame-origin
also (usefull for some CORS use cases) - Accumulates add support for aborting via AbortController #54, Add support for AbortController (#54) #68, Alternative AbortController support (#54) #137 discussions, this
unfetch
enchancement and this implementation alternative
Not sure about 500 bytes of the bundle size, but should be very close to it 😉
The code (abortable-unfetch.mjs
)
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
)
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):
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:
npm i abortcontroller-polyfill
yarn add abortcontroller-polyfill
pnpm add abortcontroller-polyfill
Then somewhere in your index.mjs
:
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
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 😄
Metadata
Metadata
Assignees
Labels
No labels