Skip to content

Commit 5d7500f

Browse files
authoredDec 16, 2024··
fix(xhr-http-handler): fix abort signal sharing for multiple requests (#6740)
1 parent bd5a3f1 commit 5d7500f

File tree

2 files changed

+70
-15
lines changed

2 files changed

+70
-15
lines changed
 

‎packages/xhr-http-handler/src/xhr-http-handler.spec.ts

+59-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { AbortSignal } from "@smithy/abort-controller";
21
import { HttpRequest } from "@smithy/protocol-http";
32
import { afterAll, afterEach, beforeAll, describe, expect, test as it, vi } from "vitest";
43

@@ -36,13 +35,19 @@ class XhrMock {
3635
this.captureArgs("getAllResponseHeaders")();
3736
return `responseHeaderKey: responseHeaderValue\r\nrk2: rv2`;
3837
}
39-
setRequestHeader = this.captureArgs("setRequestHeader");
40-
open = this.captureArgs("open");
38+
setRequestHeader(...args: any[]) {
39+
return this.captureArgs("setRequestHeader")(...args);
40+
}
41+
open(...args: any[]) {
42+
return this.captureArgs("open")(...args);
43+
}
4144
send(...args: any[]) {
4245
this.captureArgs("send")(...args);
4346
this.eventListeners["readystatechange"][0]();
4447
}
45-
abort = this.captureArgs("abort");
48+
abort(...args: any[]) {
49+
return this.captureArgs("abort")(...args);
50+
}
4651
addEventListener(...args: any[]) {
4752
this.captureArgs("addEventListener")(...args);
4853
const [event, callback] = args;
@@ -135,9 +140,9 @@ describe(XhrHttpHandler.name, () => {
135140

136141
it("should respond to AbortSignal", async () => {
137142
const handler = new XhrHttpHandler();
138-
const abortSignal = new AbortSignal();
143+
const abortController = new AbortController();
139144

140-
await handler.handle(
145+
const p1 = handler.handle(
141146
new HttpRequest({
142147
method: "PUT",
143148
hostname: "localhost",
@@ -148,21 +153,22 @@ describe(XhrHttpHandler.name, () => {
148153
protocol: "http:",
149154
path: "/api",
150155
}),
151-
{ abortSignal }
156+
{ abortSignal: abortController.signal }
152157
);
153158

154159
try {
155-
abortSignal.abort();
160+
abortController.abort();
161+
await p1;
156162
} catch (e) {
157163
expect(e.toString()).toContain("Request aborted");
158164
}
159165

160166
expect(XhrMock.captures).toEqual([
161-
["upload.addEventListener", "progress", expect.any(Function)],
162-
["addEventListener", "progress", expect.any(Function)],
163-
["addEventListener", "error", expect.any(Function)],
164-
["addEventListener", "timeout", expect.any(Function)],
165-
["addEventListener", "readystatechange", expect.any(Function)],
167+
["upload.addEventListener", "progress", expect.anything()],
168+
["addEventListener", "progress", expect.anything()],
169+
["addEventListener", "error", expect.anything()],
170+
["addEventListener", "timeout", expect.anything()],
171+
["addEventListener", "readystatechange", expect.anything()],
166172
["open", "PUT", "http://localhost:3000/api?k=v"],
167173
["setRequestHeader", "h", "1"],
168174
["send", "hello"],
@@ -171,6 +177,46 @@ describe(XhrHttpHandler.name, () => {
171177
]);
172178
});
173179

180+
it("should allow an AbortSignal to abort multiple requests", async () => {
181+
const handler = new XhrHttpHandler();
182+
const abortController = new AbortController();
183+
184+
expect(abortController.signal.addEventListener).toBeInstanceOf(Function);
185+
186+
const xhrs = [] as XMLHttpRequest[];
187+
handler.on(XhrHttpHandler.EVENTS.BEFORE_XHR_SEND, (xhr) => {
188+
xhrs.push(xhr);
189+
});
190+
191+
const request = () =>
192+
handler.handle(
193+
new HttpRequest({
194+
method: "PUT",
195+
hostname: "localhost",
196+
port: 3000,
197+
query: { k: "v" },
198+
headers: { h: "1" },
199+
body: "hello",
200+
protocol: "http:",
201+
path: "/api",
202+
}),
203+
{ abortSignal: abortController.signal }
204+
);
205+
206+
const p1 = request().catch((_) => _);
207+
const p2 = request().catch((_) => _);
208+
const p3 = request().catch((_) => _);
209+
abortController.abort();
210+
await p1;
211+
await p2;
212+
await p3;
213+
await request().catch((_) => _);
214+
await request().catch((_) => _);
215+
216+
expect(xhrs.length).toEqual(3);
217+
expect(XhrMock.captures.filter(([source]) => source === "abort")).toEqual([["abort"], ["abort"], ["abort"]]);
218+
});
219+
174220
it("should ignore forbidden request headers", async () => {
175221
const handler = new XhrHttpHandler();
176222

‎packages/xhr-http-handler/src/xhr-http-handler.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,28 @@ export class XhrHttpHandler extends EventEmitter implements HttpHandler<XhrHttpH
216216
}),
217217
requestTimeout(requestTimeoutInMs),
218218
];
219+
let removeSignalEventListener = () => {};
219220
if (abortSignal) {
220221
raceOfPromises.push(
221222
new Promise<never>((resolve, reject) => {
222-
abortSignal.onabort = () => {
223+
const onAbort = () => {
223224
xhr.abort();
224225
const abortError = new Error("Request aborted");
225226
abortError.name = "AbortError";
226227
reject(abortError);
227228
};
229+
if (typeof (abortSignal as any).addEventListener === "function") {
230+
const signal = abortSignal as any;
231+
signal.addEventListener("abort", onAbort, { once: true });
232+
removeSignalEventListener = () => signal.removeEventListener("abort", onAbort);
233+
} else {
234+
// backwards compatibility
235+
abortSignal.onabort = onAbort;
236+
}
228237
})
229238
);
230239
}
231-
return Promise.race(raceOfPromises);
240+
return Promise.race(raceOfPromises).finally(removeSignalEventListener);
232241
}
233242

234243
/**

0 commit comments

Comments
 (0)
Please sign in to comment.