Skip to content

Commit

Permalink
fix: [#1718] Fetch aborted due to timeout signal error message name i…
Browse files Browse the repository at this point in the history
…ncorrect (#1729)
  • Loading branch information
btea authored Feb 21, 2025
1 parent 6290bb9 commit 6ced6d8
Show file tree
Hide file tree
Showing 11 changed files with 220 additions and 81 deletions.
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,4 @@ export const blocking = Symbol('blocking');
export const moduleImportMap = Symbol('moduleImportMap');
export const dispatchError = Symbol('dispatchError');
export const supports = Symbol('supports');
export const reason = Symbol('reason');
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js';
import FormData from '../../form-data/FormData.js';
import HistoryScrollRestorationEnum from '../../history/HistoryScrollRestorationEnum.js';
import IHistoryItem from '../../history/IHistoryItem.js';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';

/**
* Browser frame navigation utility.
Expand Down Expand Up @@ -178,7 +179,13 @@ export default class BrowserFrameNavigator {
const readyStateManager = frame.window[PropertySymbol.readyStateManager];
const abortController = new frame.window.AbortController();
const timeout = frame.window.setTimeout(
() => abortController.abort(new Error('Request timed out.')),
() =>
abortController.abort(
new frame.window.DOMException(
'The operation was aborted. Request timed out.',
DOMExceptionNameEnum.timeoutError
)
),
goToOptions?.timeout ?? 30000
);
const finalize = (): void => {
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/src/fetch/AbortController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class AbortController {
*
* @param [reason] Reason.
*/
public abort(reason?: Error): void {
public abort(reason?: any): void {
this.signal[PropertySymbol.abort](reason);
}
}
86 changes: 61 additions & 25 deletions packages/happy-dom/src/fetch/AbortSignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export default class AbortSignal extends EventTarget {
protected declare static [PropertySymbol.window]: BrowserWindow;
protected declare [PropertySymbol.window]: BrowserWindow;

// Public properties
public readonly aborted: boolean = false;
public readonly reason: Error | null = null;
// Internal properties
public [PropertySymbol.aborted]: boolean = false;
public [PropertySymbol.reason]: any = undefined;

// Events
public onabort: ((this: AbortSignal, event: Event) => void) | null = null;
Expand All @@ -28,9 +28,7 @@ export default class AbortSignal extends EventTarget {
super();

if (!this[PropertySymbol.window]) {
throw new TypeError(
`Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.`
);
throw new TypeError(`Failed to construct 'AbortSignal': Illegal constructor`);
}
}

Expand All @@ -41,22 +39,59 @@ export default class AbortSignal extends EventTarget {
return 'AbortSignal';
}

/**
* Returns true if the signal has been aborted.
*
* @returns True if the signal has been aborted.
*/
public get aborted(): boolean {
return this[PropertySymbol.aborted];
}

/**
* Setter for aborted. Value will be ignored as the property is read-only.
*
* @param _value Aborted.
*/
public set aborted(_value: boolean) {
// Do nothing
}

/**
* Returns the reason the signal was aborted.
*
* @returns Reason.
*/
public get reason(): any {
return this[PropertySymbol.reason];
}

/**
* Setter for reason. Value will be ignored as the property is read-only.
*
* @param _value Reason.
*/
public set reason(_value: any) {
// Do nothing
}

/**
* Aborts the signal.
*
* @param [reason] Reason.
*/
public [PropertySymbol.abort](reason?: Error): void {
public [PropertySymbol.abort](reason?: any): void {
if (this.aborted) {
return;
}
(<Error>this.reason) =
reason ||
new this[PropertySymbol.window].DOMException(
'signal is aborted without reason',
DOMExceptionNameEnum.abortError
);
(<boolean>this.aborted) = true;
this[PropertySymbol.reason] =
reason !== undefined
? reason
: new this[PropertySymbol.window].DOMException(
'signal is aborted without reason',
DOMExceptionNameEnum.abortError
);
this[PropertySymbol.aborted] = true;
this.dispatchEvent(new Event('abort'));
}

Expand All @@ -75,15 +110,16 @@ export default class AbortSignal extends EventTarget {
* @param [reason] Reason.
* @returns AbortSignal instance.
*/
public static abort(reason?: Error): AbortSignal {
public static abort(reason?: any): AbortSignal {
const signal = new this();
(<Error>signal.reason) =
reason ||
new this[PropertySymbol.window].DOMException(
'signal is aborted without reason',
DOMExceptionNameEnum.abortError
);
(<boolean>signal.aborted) = true;
signal[PropertySymbol.reason] =
reason !== undefined
? reason
: new this[PropertySymbol.window].DOMException(
'signal is aborted without reason',
DOMExceptionNameEnum.abortError
);
signal[PropertySymbol.aborted] = true;
return signal;
}

Expand Down Expand Up @@ -118,8 +154,8 @@ export default class AbortSignal extends EventTarget {
*/
public static any(signals: AbortSignal[]): AbortSignal {
for (const signal of signals) {
if (signal.aborted) {
return this.abort(signal.reason);
if (signal[PropertySymbol.aborted]) {
return this.abort(signal[PropertySymbol.reason]);
}
}

Expand All @@ -135,7 +171,7 @@ export default class AbortSignal extends EventTarget {
for (const signal of signals) {
const handler = (): void => {
stopListening();
anySignal[PropertySymbol.abort](signal.reason);
anySignal[PropertySymbol.abort](signal[PropertySymbol.reason]);
};
handlers.set(signal, handler);
signal.addEventListener('abort', handler);
Expand Down
17 changes: 10 additions & 7 deletions packages/happy-dom/src/fetch/Fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,12 @@ export default class Fetch {

FetchRequestValidationUtility.validateSchema(this.request);

if (this.request.signal.aborted) {
throw new this.#window.DOMException(
'The operation was aborted.',
if (this.request.signal[PropertySymbol.aborted]) {
if (this.request.signal[PropertySymbol.reason] !== undefined) {
throw this.request.signal[PropertySymbol.reason];
}
throw new this[PropertySymbol.window].DOMException(
'signal is aborted without reason',
DOMExceptionNameEnum.abortError
);
}
Expand Down Expand Up @@ -947,8 +950,8 @@ export default class Fetch {
headers.delete('cookie2');
}

if (this.request.signal.aborted) {
this.abort();
if (this.request.signal[PropertySymbol.aborted]) {
this.abort(this.request.signal[PropertySymbol.reason]);
return true;
}

Expand Down Expand Up @@ -1006,7 +1009,7 @@ export default class Fetch {
*
* @param reason Reason.
*/
private abort(reason?: Error): void {
private abort(reason?: any): void {
const error = new this.#window.DOMException(
'The operation was aborted.' + (reason ? ' ' + reason.toString() : ''),
DOMExceptionNameEnum.abortError
Expand Down Expand Up @@ -1034,7 +1037,7 @@ export default class Fetch {
}

if (this.reject) {
this.reject(error);
this.reject(reason !== undefined ? reason : error);
}
}
}
9 changes: 6 additions & 3 deletions packages/happy-dom/src/fetch/SyncFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ export default class SyncFetch {

FetchRequestValidationUtility.validateSchema(this.request);

if (this.request.signal.aborted) {
throw new this.#window.DOMException(
'The operation was aborted.',
if (this.request.signal[PropertySymbol.aborted]) {
if (this.request.signal[PropertySymbol.reason] !== undefined) {
throw this.request.signal[PropertySymbol.reason];
}
throw new this[PropertySymbol.window].DOMException(
'signal is aborted without reason',
DOMExceptionNameEnum.abortError
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/happy-dom/test/browser/BrowserFrame.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ Task #1

expect(error).toEqual(
new DOMException(
'The operation was aborted. Error: Request timed out.',
'The operation was aborted. Request timed out.',
DOMExceptionNameEnum.abortError
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ describe('DetachedBrowserFrame', () => {

expect(error).toEqual(
new DOMException(
'The operation was aborted. Error: Request timed out.',
'The operation was aborted. Request timed out.',
DOMExceptionNameEnum.abortError
)
);
Expand Down
30 changes: 28 additions & 2 deletions packages/happy-dom/test/fetch/AbortSignal.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Event from '../../src/event/Event.js';
import { describe, it, expect, beforeEach } from 'vitest';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import * as PropertySymbol from '../../src/PropertySymbol.js';
import BrowserWindow from '../../src/window/BrowserWindow.js';
import Window from '../../src/window/Window.js';
Expand Down Expand Up @@ -49,6 +49,31 @@ describe('AbortSignal', () => {
expect(signal.aborted).toBe(true);
expect(signal.reason).toBe(reason);
});

it('Returns a new instance of AbortSignal with a default reason if no reason is provided.', () => {
const signal = window.AbortSignal.abort();

expect(signal.aborted).toBe(true);
expect(signal.reason instanceof window.DOMException).toBe(true);
expect(signal.reason.message).toBe('signal is aborted without reason');
expect(signal.reason.name).toBe('AbortError');
});

it('Returns a new instance of AbortSignal with a custom reason 1.', () => {
const signal = window.AbortSignal.abort(1);

expect(signal.aborted).toBe(true);
expect(signal.reason instanceof Error).toBe(false);
expect(signal.reason).toBe(1);
});

it('Returns a new instance of AbortSignal with a custom reason null.', () => {
const signal = window.AbortSignal.abort(null);

expect(signal.aborted).toBe(true);
expect(signal.reason instanceof Error).toBe(false);
expect(signal.reason).toBe(null);
});
});

describe('AbortSignal.timeout()', () => {
Expand All @@ -60,7 +85,8 @@ describe('AbortSignal', () => {
await new Promise((resolve) => setTimeout(resolve, 100));

expect(signal.aborted).toBe(true);
expect(signal.reason).toBeInstanceOf(DOMException);
expect(signal.reason).toBeInstanceOf(window.DOMException);
expect(signal.reason?.message).toBe('signal timed out');
expect(signal.reason?.name).toBe('TimeoutError');
});
});
Expand Down
Loading

0 comments on commit 6ced6d8

Please sign in to comment.