Skip to content

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

Description

@eugeneilyin

Resolves #54
Alternative to #68, and #137

This solution

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 😄

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions