Skip to content

Commit

Permalink
Merge pull request #592 from jamesplease/abort-polyfill
Browse files Browse the repository at this point in the history
Support abort API
  • Loading branch information
mislav authored May 23, 2018
2 parents f83e371 + c873843 commit da07fa1
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 0 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ expected to uphold this code.
* [Sending cookies](#sending-cookies)
* [Receiving cookies](#receiving-cookies)
* [Obtaining the Response URL](#obtaining-the-response-url)
* [Aborting requests](#aborting-requests)
* [Browser Support](#browser-support)

## Read this first
Expand Down Expand Up @@ -260,6 +261,36 @@ response.headers['X-Request-URL'] = request.url
This server workaround is necessary if you need reliable `response.url` in
Firefox < 32, Chrome < 37, Safari, or IE.

#### Aborting requests

This polyfill supports
[the abortable fetch API](https://developers.google.com/web/updates/2017/09/abortable-fetch).
However, aborting a fetch requires use of two additional DOM APIs:
[AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and
[AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal).
Typically, browsers that do not support fetch will also not support
AbortController or AbortSignal. Consequently, you will need to include
[an additional polyfill](https://github.com/mo/abortcontroller-polyfill#readme)
for these APIs to abort fetches:

```js
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
import {fetch} from 'whatwg-fetch'

const controller = new AbortController()

fetch('/avatars', {
signal: controller.signal
}).catch(function(ex) {
if (ex.name === 'AbortError') {
console.log('request aborted')
}
})

// some time later...
controller.abort()
```

## Browser Support

- Chrome
Expand Down
40 changes: 40 additions & 0 deletions fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export function Request(input, options) {
}
this.method = input.method
this.mode = input.mode
this.signal = input.signal
if (!body && input._bodyInit != null) {
body = input._bodyInit
input.bodyUsed = true
Expand All @@ -335,6 +336,7 @@ export function Request(input, options) {
}
this.method = normalizeMethod(options.method || this.method || 'GET')
this.mode = options.mode || this.mode || null
this.signal = options.signal || this.signal
this.referrer = null

if ((this.method === 'GET' || this.method === 'HEAD') && body) {
Expand Down Expand Up @@ -422,11 +424,34 @@ Response.redirect = function(url, status) {
return new Response(null, {status: status, headers: {location: url}})
}

export var DOMException = self.DOMException
try {
new DOMException()
} catch (err) {
DOMException = function(message, name) {
this.message = message
this.name = name
var error = Error(message)
this.stack = error.stack
}
DOMException.prototype = Object.create(Error.prototype)
DOMException.prototype.constructor = DOMException
}

export function fetch(input, init) {
return new Promise(function(resolve, reject) {
var request = new Request(input, init)

if (request.signal && request.signal.aborted) {
return reject(new DOMException('Aborted', 'AbortError'))
}

var xhr = new XMLHttpRequest()

function abortXhr() {
xhr.abort()
}

xhr.onload = function() {
var options = {
status: xhr.status,
Expand All @@ -446,6 +471,10 @@ export function fetch(input, init) {
reject(new TypeError('Network request failed'))
}

xhr.onabort = function() {
reject(new DOMException('Aborted', 'AbortError'))
}

xhr.open(request.method, request.url, true)

if (request.credentials === 'include') {
Expand All @@ -462,6 +491,17 @@ export function fetch(input, init) {
xhr.setRequestHeader(name, value)
})

if (request.signal) {
request.signal.addEventListener('abort', abortXhr)

xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
request.signal.removeEventListener('abort', abortXhr)
}
}
}

xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
})
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"repository": "github/fetch",
"license": "MIT",
"devDependencies": {
"abortcontroller-polyfill": "^1.1.9",
"chai": "1.10.0",
"eslint": "^4.19.1",
"eslint-plugin-github": "^1.0.0",
Expand Down
6 changes: 6 additions & 0 deletions script/server
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ var routes = {
res.writeHead(204);
res.end();
},
'/slow': function(res) {
setTimeout(function() {
res.writeHead(200);
res.end();
}, 100);
},
'/error': function(res) {
res.destroy();
},
Expand Down
1 change: 1 addition & 0 deletions test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</script>

<script src="/node_modules/promise-polyfill/promise.js"></script>
<script src="/node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js"></script>
<script src="/dist/fetch.umd.js"></script>
<script src="/test/test.js"></script>

Expand Down
136 changes: 136 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,142 @@ exercise.forEach(function(exerciseMode) {
})
})

suite('aborting', function() {
test('initially aborted signal', function() {
var controller = new AbortController()
controller.abort()

return fetch('/request', {
signal: controller.signal
}).then(
function() {
assert.ok(false)
},
function(error) {
assert.instanceOf(error, WHATWGFetch.DOMException)
assert.equal(error.name, 'AbortError')
}
)
})

test('initially aborted signal within Request', function() {
var controller = new AbortController()
controller.abort()

var request = new Request('/request', {signal: controller.signal})

return fetch(request).then(
function() {
assert.ok(false)
},
function(error) {
assert.equal(error.name, 'AbortError')
}
)
})

test('mid-request', function() {
var controller = new AbortController()

setTimeout(function() {
controller.abort()
}, 30)

return fetch('/slow', {
signal: controller.signal
}).then(
function() {
assert.ok(false)
},
function(error) {
assert.equal(error.name, 'AbortError')
}
)
})

test('mid-request within Request', function() {
var controller = new AbortController()
var request = new Request('/slow', {signal: controller.signal})

setTimeout(function() {
controller.abort()
}, 30)

return fetch(request).then(
function() {
assert.ok(false)
},
function(error) {
assert.equal(error.name, 'AbortError')
}
)
})

test('abort multiple with same signal', function() {
var controller = new AbortController()

setTimeout(function() {
controller.abort()
}, 30)

return Promise.all([
fetch('/slow', {
signal: controller.signal
}).then(
function() {
assert.ok(false)
},
function(error) {
assert.equal(error.name, 'AbortError')
}
),
fetch('/slow', {
signal: controller.signal
}).then(
function() {
assert.ok(false)
},
function(error) {
assert.equal(error.name, 'AbortError')
}
)
])
})

test('does not leak memory', function() {
var controller = new AbortController()
var signal = controller.signal

// success
return fetch('/request', {
signal: signal
})
.then(function() {
assert.deepEqual(signal.listeners['abort'], [])
})
.then(function() {
// failure
return fetch('/boom', {
signal: signal
}).catch(function() {
assert.deepEqual(signal.listeners['abort'], [])
})
})
.then(function() {
// aborted
setTimeout(function() {
signal.dispatchEvent({type: 'abort'})
}, 30)

return fetch('/slow', {
signal: signal
}).catch(function() {
assert.deepEqual(signal.listeners['abort'], [])
})
})
})
})

suite('response', function() {
test('populates body', function() {
return fetch('/hello')
Expand Down
1 change: 1 addition & 0 deletions test/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mocha.setup('tdd')
self.assert = chai.assert

importScripts('/node_modules/promise-polyfill/promise.js')
importScripts('/node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js')
importScripts('/dist/fetch.umd.js')
importScripts('/test/test.js')

Expand Down

0 comments on commit da07fa1

Please sign in to comment.