diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 4713ab7eb25..f9b09547edb 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -33,7 +33,8 @@ const { isBlobLike, sameOrigin, isCancelled, - isAborted + isAborted, + isErrorLike } = require('./util') const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const assert = require('assert') @@ -1854,7 +1855,7 @@ async function httpNetworkFetch ( timingInfo.decodedBodySize += bytes?.byteLength ?? 0 // 6. If bytes is failure, then terminate fetchParams’s controller. - if (bytes instanceof Error) { + if (isErrorLike(bytes)) { fetchParams.controller.terminate(bytes) return } @@ -1894,7 +1895,7 @@ async function httpNetworkFetch ( // 3. Otherwise, if stream is readable, error stream with a TypeError. if (isReadable(stream)) { fetchParams.controller.controller.error(new TypeError('terminated', { - cause: reason instanceof Error ? reason : undefined + cause: isErrorLike(reason) ? reason : undefined })) } } diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 4649a5da907..526259478d4 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -10,7 +10,8 @@ const { isCancelled, isAborted, isBlobLike, - serializeJavascriptValueToJSONString + serializeJavascriptValueToJSONString, + isErrorLike } = require('./util') const { redirectStatus, @@ -347,15 +348,15 @@ function makeResponse (init) { } function makeNetworkError (reason) { + const isError = isErrorLike(reason) return makeResponse({ type: 'error', status: 0, - error: - reason instanceof Error - ? reason - : new Error(reason ? String(reason) : reason, { - cause: reason instanceof Error ? reason : undefined - }), + error: isError + ? reason + : new Error(reason ? String(reason) : reason, { + cause: isError ? reason : undefined + }), aborted: reason && reason.name === 'AbortError' }) } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 17c68162980..9806e331871 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -82,6 +82,13 @@ function isFileLike (object) { ) } +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + // Check whether |statusText| is a ByteString and // matches the Reason-Phrase token production. // RFC 2616: https://tools.ietf.org/html/rfc2616 @@ -469,5 +476,6 @@ module.exports = { makeIterator, isValidHeaderName, isValidHeaderValue, - hasOwn + hasOwn, + isErrorLike } diff --git a/test/jest/instanceof-error.test.js b/test/jest/instanceof-error.test.js new file mode 100644 index 00000000000..0422bfac76b --- /dev/null +++ b/test/jest/instanceof-error.test.js @@ -0,0 +1,44 @@ +'use strict' + +const { createServer } = require('http') +const { once } = require('events') + +/* global expect, it, jest, AbortController */ + +// https://github.com/facebook/jest/issues/11607#issuecomment-899068995 +jest.useRealTimers() + +const runIf = (condition) => condition ? it : it.skip +const nodeMajor = Number(process.versions.node.split('.', 1)[0]) + +runIf(nodeMajor >= 16)('isErrorLike sanity check', () => { + const { isErrorLike } = require('../../lib/fetch/util') + const { DOMException } = require('../../lib/fetch/constants') + const error = new DOMException('') + + // https://github.com/facebook/jest/issues/2549 + expect(error instanceof Error).toBeFalsy() + expect(isErrorLike(error)).toBeTruthy() +}) + +runIf(nodeMajor >= 16)('Real use-case', async () => { + const { fetch } = require('../..') + + const ac = new AbortController() + ac.abort() + + const server = createServer((req, res) => { + res.end() + }).listen(0) + + await once(server, 'listening') + + const promise = fetch(`https://localhost:${server.address().port}`, { + signal: ac.signal + }) + + await expect(promise).rejects.toThrowError('The operation was aborted.') + + server.close() + await once(server, 'close') +})