Skip to content

Commit 6ced6d8

Browse files
authored
fix: [#1718] Fetch aborted due to timeout signal error message name incorrect (#1729)
1 parent 6290bb9 commit 6ced6d8

11 files changed

+220
-81
lines changed

packages/happy-dom/src/PropertySymbol.ts

+1
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,4 @@ export const blocking = Symbol('blocking');
391391
export const moduleImportMap = Symbol('moduleImportMap');
392392
export const dispatchError = Symbol('dispatchError');
393393
export const supports = Symbol('supports');
394+
export const reason = Symbol('reason');

packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js';
1010
import FormData from '../../form-data/FormData.js';
1111
import HistoryScrollRestorationEnum from '../../history/HistoryScrollRestorationEnum.js';
1212
import IHistoryItem from '../../history/IHistoryItem.js';
13+
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
1314

1415
/**
1516
* Browser frame navigation utility.
@@ -178,7 +179,13 @@ export default class BrowserFrameNavigator {
178179
const readyStateManager = frame.window[PropertySymbol.readyStateManager];
179180
const abortController = new frame.window.AbortController();
180181
const timeout = frame.window.setTimeout(
181-
() => abortController.abort(new Error('Request timed out.')),
182+
() =>
183+
abortController.abort(
184+
new frame.window.DOMException(
185+
'The operation was aborted. Request timed out.',
186+
DOMExceptionNameEnum.timeoutError
187+
)
188+
),
182189
goToOptions?.timeout ?? 30000
183190
);
184191
const finalize = (): void => {

packages/happy-dom/src/fetch/AbortController.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class AbortController {
1919
*
2020
* @param [reason] Reason.
2121
*/
22-
public abort(reason?: Error): void {
22+
public abort(reason?: any): void {
2323
this.signal[PropertySymbol.abort](reason);
2424
}
2525
}

packages/happy-dom/src/fetch/AbortSignal.ts

+61-25
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ export default class AbortSignal extends EventTarget {
1414
protected declare static [PropertySymbol.window]: BrowserWindow;
1515
protected declare [PropertySymbol.window]: BrowserWindow;
1616

17-
// Public properties
18-
public readonly aborted: boolean = false;
19-
public readonly reason: Error | null = null;
17+
// Internal properties
18+
public [PropertySymbol.aborted]: boolean = false;
19+
public [PropertySymbol.reason]: any = undefined;
2020

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

3030
if (!this[PropertySymbol.window]) {
31-
throw new TypeError(
32-
`Failed to construct '${this.constructor.name}': '${this.constructor.name}' was constructed outside a Window context.`
33-
);
31+
throw new TypeError(`Failed to construct 'AbortSignal': Illegal constructor`);
3432
}
3533
}
3634

@@ -41,22 +39,59 @@ export default class AbortSignal extends EventTarget {
4139
return 'AbortSignal';
4240
}
4341

42+
/**
43+
* Returns true if the signal has been aborted.
44+
*
45+
* @returns True if the signal has been aborted.
46+
*/
47+
public get aborted(): boolean {
48+
return this[PropertySymbol.aborted];
49+
}
50+
51+
/**
52+
* Setter for aborted. Value will be ignored as the property is read-only.
53+
*
54+
* @param _value Aborted.
55+
*/
56+
public set aborted(_value: boolean) {
57+
// Do nothing
58+
}
59+
60+
/**
61+
* Returns the reason the signal was aborted.
62+
*
63+
* @returns Reason.
64+
*/
65+
public get reason(): any {
66+
return this[PropertySymbol.reason];
67+
}
68+
69+
/**
70+
* Setter for reason. Value will be ignored as the property is read-only.
71+
*
72+
* @param _value Reason.
73+
*/
74+
public set reason(_value: any) {
75+
// Do nothing
76+
}
77+
4478
/**
4579
* Aborts the signal.
4680
*
4781
* @param [reason] Reason.
4882
*/
49-
public [PropertySymbol.abort](reason?: Error): void {
83+
public [PropertySymbol.abort](reason?: any): void {
5084
if (this.aborted) {
5185
return;
5286
}
53-
(<Error>this.reason) =
54-
reason ||
55-
new this[PropertySymbol.window].DOMException(
56-
'signal is aborted without reason',
57-
DOMExceptionNameEnum.abortError
58-
);
59-
(<boolean>this.aborted) = true;
87+
this[PropertySymbol.reason] =
88+
reason !== undefined
89+
? reason
90+
: new this[PropertySymbol.window].DOMException(
91+
'signal is aborted without reason',
92+
DOMExceptionNameEnum.abortError
93+
);
94+
this[PropertySymbol.aborted] = true;
6095
this.dispatchEvent(new Event('abort'));
6196
}
6297

@@ -75,15 +110,16 @@ export default class AbortSignal extends EventTarget {
75110
* @param [reason] Reason.
76111
* @returns AbortSignal instance.
77112
*/
78-
public static abort(reason?: Error): AbortSignal {
113+
public static abort(reason?: any): AbortSignal {
79114
const signal = new this();
80-
(<Error>signal.reason) =
81-
reason ||
82-
new this[PropertySymbol.window].DOMException(
83-
'signal is aborted without reason',
84-
DOMExceptionNameEnum.abortError
85-
);
86-
(<boolean>signal.aborted) = true;
115+
signal[PropertySymbol.reason] =
116+
reason !== undefined
117+
? reason
118+
: new this[PropertySymbol.window].DOMException(
119+
'signal is aborted without reason',
120+
DOMExceptionNameEnum.abortError
121+
);
122+
signal[PropertySymbol.aborted] = true;
87123
return signal;
88124
}
89125

@@ -118,8 +154,8 @@ export default class AbortSignal extends EventTarget {
118154
*/
119155
public static any(signals: AbortSignal[]): AbortSignal {
120156
for (const signal of signals) {
121-
if (signal.aborted) {
122-
return this.abort(signal.reason);
157+
if (signal[PropertySymbol.aborted]) {
158+
return this.abort(signal[PropertySymbol.reason]);
123159
}
124160
}
125161

@@ -135,7 +171,7 @@ export default class AbortSignal extends EventTarget {
135171
for (const signal of signals) {
136172
const handler = (): void => {
137173
stopListening();
138-
anySignal[PropertySymbol.abort](signal.reason);
174+
anySignal[PropertySymbol.abort](signal[PropertySymbol.reason]);
139175
};
140176
handlers.set(signal, handler);
141177
signal.addEventListener('abort', handler);

packages/happy-dom/src/fetch/Fetch.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,12 @@ export default class Fetch {
134134

135135
FetchRequestValidationUtility.validateSchema(this.request);
136136

137-
if (this.request.signal.aborted) {
138-
throw new this.#window.DOMException(
139-
'The operation was aborted.',
137+
if (this.request.signal[PropertySymbol.aborted]) {
138+
if (this.request.signal[PropertySymbol.reason] !== undefined) {
139+
throw this.request.signal[PropertySymbol.reason];
140+
}
141+
throw new this[PropertySymbol.window].DOMException(
142+
'signal is aborted without reason',
140143
DOMExceptionNameEnum.abortError
141144
);
142145
}
@@ -947,8 +950,8 @@ export default class Fetch {
947950
headers.delete('cookie2');
948951
}
949952

950-
if (this.request.signal.aborted) {
951-
this.abort();
953+
if (this.request.signal[PropertySymbol.aborted]) {
954+
this.abort(this.request.signal[PropertySymbol.reason]);
952955
return true;
953956
}
954957

@@ -1006,7 +1009,7 @@ export default class Fetch {
10061009
*
10071010
* @param reason Reason.
10081011
*/
1009-
private abort(reason?: Error): void {
1012+
private abort(reason?: any): void {
10101013
const error = new this.#window.DOMException(
10111014
'The operation was aborted.' + (reason ? ' ' + reason.toString() : ''),
10121015
DOMExceptionNameEnum.abortError
@@ -1034,7 +1037,7 @@ export default class Fetch {
10341037
}
10351038

10361039
if (this.reject) {
1037-
this.reject(error);
1040+
this.reject(reason !== undefined ? reason : error);
10381041
}
10391042
}
10401043
}

packages/happy-dom/src/fetch/SyncFetch.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,12 @@ export default class SyncFetch {
113113

114114
FetchRequestValidationUtility.validateSchema(this.request);
115115

116-
if (this.request.signal.aborted) {
117-
throw new this.#window.DOMException(
118-
'The operation was aborted.',
116+
if (this.request.signal[PropertySymbol.aborted]) {
117+
if (this.request.signal[PropertySymbol.reason] !== undefined) {
118+
throw this.request.signal[PropertySymbol.reason];
119+
}
120+
throw new this[PropertySymbol.window].DOMException(
121+
'signal is aborted without reason',
119122
DOMExceptionNameEnum.abortError
120123
);
121124
}

packages/happy-dom/test/browser/BrowserFrame.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ Task #1
352352

353353
expect(error).toEqual(
354354
new DOMException(
355-
'The operation was aborted. Error: Request timed out.',
355+
'The operation was aborted. Request timed out.',
356356
DOMExceptionNameEnum.abortError
357357
)
358358
);

packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ describe('DetachedBrowserFrame', () => {
329329

330330
expect(error).toEqual(
331331
new DOMException(
332-
'The operation was aborted. Error: Request timed out.',
332+
'The operation was aborted. Request timed out.',
333333
DOMExceptionNameEnum.abortError
334334
)
335335
);

packages/happy-dom/test/fetch/AbortSignal.test.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Event from '../../src/event/Event.js';
2-
import { describe, it, expect, beforeEach } from 'vitest';
2+
import { describe, it, expect, beforeEach, vi } from 'vitest';
33
import * as PropertySymbol from '../../src/PropertySymbol.js';
44
import BrowserWindow from '../../src/window/BrowserWindow.js';
55
import Window from '../../src/window/Window.js';
@@ -49,6 +49,31 @@ describe('AbortSignal', () => {
4949
expect(signal.aborted).toBe(true);
5050
expect(signal.reason).toBe(reason);
5151
});
52+
53+
it('Returns a new instance of AbortSignal with a default reason if no reason is provided.', () => {
54+
const signal = window.AbortSignal.abort();
55+
56+
expect(signal.aborted).toBe(true);
57+
expect(signal.reason instanceof window.DOMException).toBe(true);
58+
expect(signal.reason.message).toBe('signal is aborted without reason');
59+
expect(signal.reason.name).toBe('AbortError');
60+
});
61+
62+
it('Returns a new instance of AbortSignal with a custom reason 1.', () => {
63+
const signal = window.AbortSignal.abort(1);
64+
65+
expect(signal.aborted).toBe(true);
66+
expect(signal.reason instanceof Error).toBe(false);
67+
expect(signal.reason).toBe(1);
68+
});
69+
70+
it('Returns a new instance of AbortSignal with a custom reason null.', () => {
71+
const signal = window.AbortSignal.abort(null);
72+
73+
expect(signal.aborted).toBe(true);
74+
expect(signal.reason instanceof Error).toBe(false);
75+
expect(signal.reason).toBe(null);
76+
});
5277
});
5378

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

6287
expect(signal.aborted).toBe(true);
63-
expect(signal.reason).toBeInstanceOf(DOMException);
88+
expect(signal.reason).toBeInstanceOf(window.DOMException);
89+
expect(signal.reason?.message).toBe('signal timed out');
6490
expect(signal.reason?.name).toBe('TimeoutError');
6591
});
6692
});

0 commit comments

Comments
 (0)