Skip to content

Commit 95cdd3e

Browse files
committed
chore: [capricorn86#1718] Adds fixes related to AbortSignal and Fetch
1 parent c552220 commit 95cdd3e

11 files changed

+197
-97
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

+49-15
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 reason: any = 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,6 +39,42 @@ 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
*
@@ -50,14 +84,14 @@ export default class AbortSignal extends EventTarget {
5084
if (this.aborted) {
5185
return;
5286
}
53-
this.reason =
87+
this[PropertySymbol.reason] =
5488
reason !== undefined
5589
? reason
5690
: new this[PropertySymbol.window].DOMException(
5791
'signal is aborted without reason',
5892
DOMExceptionNameEnum.abortError
59-
);
60-
(<boolean>this.aborted) = true;
93+
);
94+
this[PropertySymbol.aborted] = true;
6195
this.dispatchEvent(new Event('abort'));
6296
}
6397

@@ -78,14 +112,14 @@ export default class AbortSignal extends EventTarget {
78112
*/
79113
public static abort(reason?: any): AbortSignal {
80114
const signal = new this();
81-
signal.reason =
115+
signal[PropertySymbol.reason] =
82116
reason !== undefined
83117
? reason
84118
: new this[PropertySymbol.window].DOMException(
85119
'signal is aborted without reason',
86120
DOMExceptionNameEnum.abortError
87-
);
88-
(<boolean>signal.aborted) = true;
121+
);
122+
signal[PropertySymbol.aborted] = true;
89123
return signal;
90124
}
91125

@@ -120,8 +154,8 @@ export default class AbortSignal extends EventTarget {
120154
*/
121155
public static any(signals: AbortSignal[]): AbortSignal {
122156
for (const signal of signals) {
123-
if (signal.aborted) {
124-
return this.abort(signal.reason);
157+
if (signal[PropertySymbol.aborted]) {
158+
return this.abort(signal[PropertySymbol.reason]);
125159
}
126160
}
127161

@@ -137,7 +171,7 @@ export default class AbortSignal extends EventTarget {
137171
for (const signal of signals) {
138172
const handler = (): void => {
139173
stopListening();
140-
anySignal[PropertySymbol.abort](signal.reason);
174+
anySignal[PropertySymbol.abort](signal[PropertySymbol.reason]);
141175
};
142176
handlers.set(signal, handler);
143177
signal.addEventListener('abort', handler);

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

+16-16
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,14 @@ export default class Fetch {
134134

135135
FetchRequestValidationUtility.validateSchema(this.request);
136136

137-
if (this.request.signal.aborted) {
138-
if (this.request.signal.reason instanceof Error) {
139-
throw new this.#window.DOMException(
140-
this.request.signal.reason.message || 'The operation was aborted.',
141-
this.request.signal.reason.name || DOMExceptionNameEnum.abortError
142-
);
137+
if (this.request.signal[PropertySymbol.aborted]) {
138+
if (this.request.signal[PropertySymbol.reason] !== undefined) {
139+
throw this.request.signal[PropertySymbol.reason];
143140
}
144-
throw this.request.signal.reason;
141+
throw new this[PropertySymbol.window].DOMException(
142+
'signal is aborted without reason',
143+
DOMExceptionNameEnum.abortError
144+
);
145145
}
146146

147147
if (this.request[PropertySymbol.url].protocol === 'data:') {
@@ -154,7 +154,7 @@ export default class Fetch {
154154
window: this.#window,
155155
response: this.response,
156156
request: this.request
157-
})
157+
})
158158
: undefined;
159159
return interceptedResponse instanceof Response ? interceptedResponse : this.response;
160160
}
@@ -341,7 +341,7 @@ export default class Fetch {
341341
window: this.#window,
342342
response: await response,
343343
request: this.request
344-
})
344+
})
345345
: undefined;
346346
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
347347
return interceptedResponse instanceof Response ? interceptedResponse : response;
@@ -364,7 +364,7 @@ export default class Fetch {
364364
window: this.#window,
365365
response: await response,
366366
request: this.request
367-
})
367+
})
368368
: undefined;
369369
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
370370
return interceptedResponse instanceof Response ? interceptedResponse : response;
@@ -388,7 +388,7 @@ export default class Fetch {
388388
window: this.#window,
389389
response: await response,
390390
request: this.request
391-
})
391+
})
392392
: undefined;
393393

394394
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
@@ -545,7 +545,7 @@ export default class Fetch {
545545
window: this.#window,
546546
response: await response,
547547
request: this.request
548-
})
548+
})
549549
: undefined;
550550
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
551551
const returnResponse =
@@ -950,8 +950,8 @@ export default class Fetch {
950950
headers.delete('cookie2');
951951
}
952952

953-
if (this.request.signal.aborted) {
954-
this.abort();
953+
if (this.request.signal[PropertySymbol.aborted]) {
954+
this.abort(this.request.signal[PropertySymbol.reason]);
955955
return true;
956956
}
957957

@@ -1009,7 +1009,7 @@ export default class Fetch {
10091009
*
10101010
* @param reason Reason.
10111011
*/
1012-
private abort(reason?: Error): void {
1012+
private abort(reason?: any): void {
10131013
const error = new this.#window.DOMException(
10141014
'The operation was aborted.' + (reason ? ' ' + reason.toString() : ''),
10151015
DOMExceptionNameEnum.abortError
@@ -1037,7 +1037,7 @@ export default class Fetch {
10371037
}
10381038

10391039
if (this.reject) {
1040-
this.reject(error);
1040+
this.reject(reason !== undefined ? reason : error);
10411041
}
10421042
}
10431043
}

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

+12-9
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export default class SyncFetch {
104104
? this.interceptor.beforeSyncRequest({
105105
request: this.request,
106106
window: this.#window
107-
})
107+
})
108108
: undefined;
109109

110110
if (typeof beforeRequestResponse === 'object') {
@@ -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
}
@@ -136,7 +139,7 @@ export default class SyncFetch {
136139
window: this.#window,
137140
response,
138141
request: this.request
139-
})
142+
})
140143
: undefined;
141144
return typeof interceptedResponse === 'object' ? interceptedResponse : response;
142145
}
@@ -290,7 +293,7 @@ export default class SyncFetch {
290293
window: this.#window,
291294
response,
292295
request: this.request
293-
})
296+
})
294297
: undefined;
295298
return typeof interceptedResponse === 'object' ? interceptedResponse : response;
296299
}
@@ -309,7 +312,7 @@ export default class SyncFetch {
309312
window: this.#window,
310313
response,
311314
request: this.request
312-
})
315+
})
313316
: undefined;
314317
return typeof interceptedResponse === 'object' ? interceptedResponse : response;
315318
}
@@ -328,7 +331,7 @@ export default class SyncFetch {
328331
window: this.#window,
329332
response,
330333
request: this.request
331-
})
334+
})
332335
: undefined;
333336
const returnResponse = typeof interceptedResponse === 'object' ? interceptedResponse : response;
334337

@@ -525,7 +528,7 @@ export default class SyncFetch {
525528
window: this.#window,
526529
response: redirectedResponse,
527530
request: this.request
528-
})
531+
})
529532
: undefined;
530533
const returnResponse =
531534
typeof interceptedResponse === 'object' ? interceptedResponse : redirectedResponse;

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

+4-13
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ describe('AbortSignal', () => {
5454
const signal = window.AbortSignal.abort();
5555

5656
expect(signal.aborted).toBe(true);
57-
expect(signal.reason instanceof Error).toBe(true);
57+
expect(signal.reason instanceof window.DOMException).toBe(true);
58+
expect(signal.reason.message).toBe('signal is aborted without reason');
5859
expect(signal.reason.name).toBe('AbortError');
5960
});
6061

@@ -84,20 +85,10 @@ describe('AbortSignal', () => {
8485
await new Promise((resolve) => setTimeout(resolve, 100));
8586

8687
expect(signal.aborted).toBe(true);
87-
expect(signal.reason).toBeInstanceOf(DOMException);
88+
expect(signal.reason).toBeInstanceOf(window.DOMException);
89+
expect(signal.reason?.message).toBe('signal timed out');
8890
expect(signal.reason?.name).toBe('TimeoutError');
8991
});
90-
91-
it('After Abortsignal timeout, sending a request with the wrong name still being "TimeoutError" ', async () => {
92-
try {
93-
const signal = AbortSignal.timeout(20);
94-
const now = Date.now();
95-
await vi.waitUntil(() => Date.now() - now > 100);
96-
await fetch('https://example.com', { signal });
97-
} catch (e) {
98-
expect((<Error>e).name).toStrictEqual('TimeoutError');
99-
}
100-
});
10192
});
10293

10394
describe('AbortSignal.any()', () => {

0 commit comments

Comments
 (0)