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 AbortSignal.reason #1775

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions @types/index.d.ts
Expand Up @@ -13,6 +13,7 @@ import {

type AbortSignal = {
readonly aborted: boolean;
readonly reason: unknown;

addEventListener: (type: 'abort', listener: (this: AbortSignal) => void) => void;
removeEventListener: (type: 'abort', listener: (this: AbortSignal) => void) => void;
Expand Down Expand Up @@ -212,6 +213,7 @@ export class FetchError extends Error {
export class AbortError extends Error {
type: string;
name: 'AbortError';
reason: DOMException | unknown;
[Symbol.toStringTag]: 'AbortError';
}

Expand Down
6 changes: 4 additions & 2 deletions src/errors/abort-error.js
Expand Up @@ -4,7 +4,9 @@ import {FetchBaseError} from './base.js';
* AbortError interface for cancelled requests
*/
export class AbortError extends FetchBaseError {
constructor(message, type = 'aborted') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a breaking change?

AbortError is exported publicly from the package...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, wasn't aware the constructor was actually exported. It just seemed odd that the type would be explicitly given, i.e. what else could it be.

We basically need to thread the reason into the thrown AbortError. To not break its constructor, the only sane option would be to extend it:

  // no change to existing `AbortError`
  class AbortWithReasonError extends AbortError {
    constructor(message, reason) {
      super(message);
      this.reason = reason;
    }
  }

super(message, type);
constructor(message, reason) {
super(message, 'aborted');

this.reason = reason;
}
}
2 changes: 1 addition & 1 deletion src/index.js
Expand Up @@ -67,7 +67,7 @@ export default async function fetch(url, options_) {
let response = null;

const abort = () => {
const error = new AbortError('The operation was aborted.');
const error = new AbortError('The operation was aborted.', signal.reason);
reject(error);
if (request.body && request.body instanceof Stream.Readable) {
request.body.destroy(error);
Expand Down
58 changes: 56 additions & 2 deletions test/main.js
Expand Up @@ -18,6 +18,7 @@ import chaiString from 'chai-string';
import FormData from 'form-data';

import fetch, {
AbortError,
Blob,
FetchError,
fileFromSync,
Expand Down Expand Up @@ -1235,8 +1236,61 @@ describe('node-fetch', () => {

testAbortController('mysticatea', () => new AbortControllerMysticatea());

if (process.version > 'v15') {
testAbortController('native', () => new AbortController());
// process.version is of the format `v${number}.${number}.${number}`
const nodeVersion = process.version.slice(1).split('.').map(n => Number(n));

if (nodeVersion >= [15, 0, 0]) {
testAbortController(
'native',
() => new AbortController(),
() => {
if (nodeVersion >= [17, 0, 2]) {
it('should include the given reason on the thrown AbortError', () => {
const controller = new AbortController();
const promise = fetch(`${base}timeout`, {
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
body: '{"hello": "world"}'
}
});

controller.abort('the reason');

return expect(promise)
.to.eventually.be.rejected
.and.be.an.instanceOf(AbortError)
.and.to.have.property('reason')
.equals('the reason');
});

it('should include default reason on the thrown AbortError when not given', () => {
const controller = new AbortController();
const promise = fetch(`${base}timeout`, {
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
body: '{"hello": "world"}'
}
});

controller.abort();

return expect(promise)
.to.eventually.be.rejected
.and.be.an.instanceOf(AbortError)
.and.to.have.property('reason')
.that.is.an.instanceOf(DOMException)
.and.include({
message: 'This operation was aborted',
name: 'AbortError',
code: DOMException.ABORT_ERR
});
});
}
});
}

it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => {
Expand Down