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

Support abort API #592

Merged
merged 27 commits into from
May 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
93e9cec
Support abort API
jamesplease Dec 7, 2017
53565c1
Support abort without replacing native fetch
jamesplease Dec 7, 2017
9a7d232
Remove event listener; use request.signal
jamesplease Dec 7, 2017
fd5c261
Skip creation of XHR if signal is already aborted
jamesplease Dec 7, 2017
318e95f
First pass at documentation
jamesplease Dec 8, 2017
0e1b651
More tweaks based on feedback
jamesplease Dec 8, 2017
d47f6c9
Remove some semicolons
jamesplease Dec 8, 2017
529c9fa
Add `signal` property to Request instead of Response
mislav Dec 13, 2017
8ba0cdb
Polyfill DOMException if missing
mislav Dec 13, 2017
074c89e
Polyfill DOMException
mislav Dec 13, 2017
b832229
Add tests for aborting requests
mislav Dec 13, 2017
c6aff30
Avoid creating XMLHttpRequest if we're immediately aborting
mislav Dec 13, 2017
d9aa590
Clear event listener for all xhr completion status.
joaovieira Feb 2, 2018
886a076
Merge pull request #1 from joaovieira/abort-polyfill
joaovieira Feb 2, 2018
9f9a00f
Test signal reuse. Add AbortSignal polyfill.
joaovieira Feb 2, 2018
bab0f3d
Consistent implementation of EventTarget as in MDN.
joaovieira Feb 2, 2018
0bed2ba
Split EventTarget and AbortSignal polyfills.
joaovieira Feb 3, 2018
66ed8ba
Merge pull request #2 from joaovieira/abort-polyfill
joaovieira Feb 3, 2018
738d51a
Add tests with signal within Request object.
joaovieira Feb 3, 2018
8f778f3
Fix EventTarget callback invocation context.
joaovieira Feb 3, 2018
f0e24f8
Native EventTarget is not inheritable.
joaovieira Feb 3, 2018
36bf692
Replace AbortSignal polyfill with abortcontroller-polyfill module.
joaovieira Feb 4, 2018
7f6b8d2
Upgrade abortcontroller-polyfill.
joaovieira Feb 4, 2018
c31fca9
Merge remote-tracking branch 'origin/master' into abort-polyfill
mislav May 23, 2018
f035860
Scope abortcontroller to polyfill only
mislav May 23, 2018
2bd7f22
Export DOMException for `instanceof` checks
mislav May 23, 2018
c873843
Polish documentation for aborting requests
mislav May 23, 2018
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
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