Skip to content

Commit c7f527b

Browse files
authored
fix(fetch): use AbortError DOMException (#1511)
1 parent 6b7d676 commit c7f527b

File tree

5 files changed

+74
-20
lines changed

5 files changed

+74
-20
lines changed

lib/core/errors.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
'use strict'
22

3-
class AbortError extends Error {
4-
constructor () {
5-
super('The operation was aborted')
6-
this.code = 'ABORT_ERR'
7-
this.name = 'AbortError'
8-
}
9-
}
10-
113
class UndiciError extends Error {
124
constructor (message) {
135
super(message)
@@ -192,7 +184,6 @@ class HTTPParserError extends Error {
192184
}
193185

194186
module.exports = {
195-
AbortError,
196187
HTTPParserError,
197188
UndiciError,
198189
HeadersTimeoutError,

lib/fetch/constants.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,19 @@ const subresource = [
6060
''
6161
]
6262

63+
/** @type {globalThis['DOMException']} */
64+
const DOMException = globalThis.DOMException ?? (() => {
65+
// DOMException was only made a global in Node v17.0.0,
66+
// but fetch supports >= v16.5.
67+
try {
68+
atob('~')
69+
} catch (err) {
70+
return Object.getPrototypeOf(err).constructor
71+
}
72+
})()
73+
6374
module.exports = {
75+
DOMException,
6476
subresource,
6577
forbiddenMethods,
6678
requestBodyHeader,

lib/fetch/index.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ const {
3636
isAborted
3737
} = require('./util')
3838
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
39-
const { AbortError } = require('../core/errors')
4039
const assert = require('assert')
4140
const { safelyExtractBody, extractBody } = require('./body')
4241
const {
4342
redirectStatus,
4443
nullBodyStatus,
4544
safeMethods,
4645
requestBodyHeader,
47-
subresource
46+
subresource,
47+
DOMException
4848
} = require('./constants')
4949
const { kHeadersList } = require('../core/symbols')
5050
const EE = require('events')
@@ -82,7 +82,7 @@ class Fetch extends EE {
8282
return
8383
}
8484

85-
const reason = new AbortError()
85+
const reason = new DOMException('The operation was aborted.', 'AbortError')
8686

8787
this.state = 'aborted'
8888
this.connection?.destroy(reason)
@@ -295,7 +295,7 @@ function markResourceTiming () {
295295
// https://fetch.spec.whatwg.org/#abort-fetch
296296
function abortFetch (p, request, responseObject) {
297297
// 1. Let error be an "AbortError" DOMException.
298-
const error = new AbortError()
298+
const error = new DOMException('The operation was aborted.', 'AbortError')
299299

300300
// 2. Reject promise with error.
301301
p.reject(error)
@@ -1555,7 +1555,7 @@ async function httpNetworkFetch (
15551555
destroy (err) {
15561556
if (!this.destroyed) {
15571557
this.destroyed = true
1558-
this.abort?.(err ?? new AbortError())
1558+
this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
15591559
}
15601560
}
15611561
}
@@ -1885,7 +1885,9 @@ async function httpNetworkFetch (
18851885

18861886
// 2. If stream is readable, error stream with an "AbortError" DOMException.
18871887
if (isReadable(stream)) {
1888-
fetchParams.controller.controller.error(new AbortError())
1888+
fetchParams.controller.controller.error(
1889+
new DOMException('The operation was aborted.', 'AbortError')
1890+
)
18891891
}
18901892
} else {
18911893
// 3. Otherwise, if stream is readable, error stream with a TypeError.
@@ -1926,7 +1928,7 @@ async function httpNetworkFetch (
19261928
const { connection } = fetchParams.controller
19271929

19281930
if (connection.destroyed) {
1929-
abort(new AbortError())
1931+
abort(new DOMException('The operation was aborted.', 'AbortError'))
19301932
} else {
19311933
fetchParams.controller.on('terminated', abort)
19321934
this.abort = connection.abort = abort

lib/fetch/response.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict'
22

33
const { Headers, HeadersList, fill } = require('./headers')
4-
const { AbortError } = require('../core/errors')
54
const { extractBody, cloneBody, mixinBody } = require('./body')
65
const util = require('../core/util')
76
const { kEnumerableProperty } = util
@@ -15,7 +14,8 @@ const {
1514
} = require('./util')
1615
const {
1716
redirectStatus,
18-
nullBodyStatus
17+
nullBodyStatus,
18+
DOMException
1919
} = require('./constants')
2020
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
2121
const { webidl } = require('./webidl')
@@ -440,7 +440,7 @@ function makeAppropriateNetworkError (fetchParams) {
440440
// 2. Return an aborted network error if fetchParams is aborted;
441441
// otherwise return a network error.
442442
return isAborted(fetchParams)
443-
? makeNetworkError(new AbortError())
443+
? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError'))
444444
: makeNetworkError(fetchParams.controller.terminated.reason)
445445
}
446446

test/fetch/abort.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const { test } = require('tap')
44
const { fetch } = require('../..')
55
const { createServer } = require('http')
66
const { once } = require('events')
7+
const { ReadableStream } = require('stream/web')
8+
const { DOMException } = require('../../lib/fetch/constants')
79

810
/* global AbortController */
911

@@ -52,8 +54,55 @@ test('parallel fetch with the same AbortController works as expected', async (t)
5254
t.equal(rejected.length, 9) // out of 10 requests, only 1 should succeed
5355
t.equal(resolved.length, 1)
5456

55-
t.ok(rejected.every(rej => rej.reason?.code === 'ABORT_ERR'))
57+
t.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR))
5658
t.same(resolved[0].value, body)
5759

5860
t.end()
5961
})
62+
63+
// https://github.com/web-platform-tests/wpt/blob/fd8aeb1bb2eb33bc43f8a5bbc682b0cff6075dfe/fetch/api/abort/general.any.js#L474-L507
64+
test('Readable stream synchronously cancels with AbortError if aborted before reading', async (t) => {
65+
const server = createServer((req, res) => {
66+
res.write('')
67+
res.end()
68+
}).listen(0)
69+
70+
t.teardown(server.close.bind(server))
71+
await once(server, 'listening')
72+
73+
const controller = new AbortController()
74+
const signal = controller.signal
75+
controller.abort()
76+
77+
let cancelReason
78+
79+
const body = new ReadableStream({
80+
pull (controller) {
81+
controller.enqueue(new Uint8Array([42]))
82+
},
83+
cancel (reason) {
84+
cancelReason = reason
85+
}
86+
})
87+
88+
const fetchPromise = fetch(`http://localhost:${server.address().port}`, {
89+
body,
90+
signal,
91+
method: 'POST',
92+
headers: {
93+
'Content-Type': 'text/plain'
94+
}
95+
})
96+
97+
t.ok(cancelReason, 'Cancel called sync')
98+
t.equal(cancelReason.constructor, DOMException)
99+
t.equal(cancelReason.name, 'AbortError')
100+
101+
await t.rejects(fetchPromise, { name: 'AbortError' })
102+
103+
const fetchErr = await fetchPromise.catch(e => e)
104+
105+
t.equal(cancelReason, fetchErr, 'Fetch rejects with same error instance')
106+
107+
t.end()
108+
})

0 commit comments

Comments
 (0)