Skip to content

Commit 9c12aff

Browse files
authored
feat(xhr-http-handler): add support for per-request/per-operation timeouts (#7159)
* feat(xhr-http-handler): add support for per-request/per-operation timeouts * test(xhr-http-handler): refactor tests and cleanups * test(xhr-http-handler): cleanups
1 parent 5d25600 commit 9c12aff

File tree

2 files changed

+95
-5
lines changed

2 files changed

+95
-5
lines changed

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

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class XhrMock {
1414
public static captures: any[] = [];
1515
public static DONE = 4;
1616

17-
private captureArgs =
17+
protected captureArgs =
1818
(caller: string) =>
1919
(...args: any[]) => {
2020
XhrMock.captures.push([caller, ...args]);
@@ -244,4 +244,94 @@ describe(XhrHttpHandler.name, () => {
244244
["getAllResponseHeaders"],
245245
]);
246246
});
247+
248+
describe("per-request requestTimeout", () => {
249+
it("should use per-request timeout over handler config timeout", async () => {
250+
const handler = new XhrHttpHandler({ requestTimeout: 5000 });
251+
252+
const requestTimeoutSpy = vi.spyOn(await import("./request-timeout"), "requestTimeout");
253+
254+
const mockRequest = new HttpRequest({
255+
method: "GET",
256+
hostname: "example.com",
257+
protocol: "https:",
258+
path: "/",
259+
headers: {},
260+
});
261+
262+
class TimeoutXhrMock extends XhrMock {
263+
send(...args: any[]) {
264+
this.captureArgs("send")(...args);
265+
// let it timeout
266+
}
267+
}
268+
269+
(global as any).XMLHttpRequest = TimeoutXhrMock;
270+
271+
try {
272+
await handler.handle(mockRequest, { requestTimeout: 100 });
273+
} catch (error) {
274+
// expected to timeout
275+
}
276+
277+
// verify requestTimeout function was called with per-request timeout (100), not handler timeout (5000)
278+
expect(requestTimeoutSpy).toHaveBeenCalledWith(100);
279+
280+
requestTimeoutSpy.mockRestore();
281+
(global as any).XMLHttpRequest = XhrMock; // restore original mock
282+
});
283+
284+
it("should fall back to handler config timeout when per-request timeout not provided", async () => {
285+
const handler = new XhrHttpHandler({ requestTimeout: 200 });
286+
287+
const requestTimeoutSpy = vi.spyOn(await import("./request-timeout"), "requestTimeout");
288+
289+
const mockRequest = new HttpRequest({
290+
method: "GET",
291+
hostname: "example.com",
292+
protocol: "https:",
293+
path: "/",
294+
headers: {},
295+
});
296+
297+
class TimeoutXhrMock extends XhrMock {
298+
send(...args: any[]) {
299+
this.captureArgs("send")(...args);
300+
}
301+
}
302+
303+
(global as any).XMLHttpRequest = TimeoutXhrMock;
304+
305+
try {
306+
await handler.handle(mockRequest, {});
307+
} catch (error) {}
308+
309+
expect(requestTimeoutSpy).toHaveBeenCalledWith(200);
310+
311+
requestTimeoutSpy.mockRestore();
312+
(global as any).XMLHttpRequest = XhrMock;
313+
});
314+
315+
it("should handle zero timeout correctly", async () => {
316+
const handler = new XhrHttpHandler({ requestTimeout: 1000 });
317+
318+
const requestTimeoutSpy = vi.spyOn(await import("./request-timeout"), "requestTimeout");
319+
320+
const mockRequest = new HttpRequest({
321+
method: "GET",
322+
hostname: "example.com",
323+
protocol: "https:",
324+
path: "/",
325+
headers: {},
326+
});
327+
328+
try {
329+
await handler.handle(mockRequest, { requestTimeout: 0 });
330+
} catch (error) {}
331+
332+
expect(requestTimeoutSpy).toHaveBeenCalledWith(0);
333+
334+
requestTimeoutSpy.mockRestore();
335+
});
336+
});
247337
});

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { buildQueryString } from "@smithy/querystring-builder";
33
import { HttpHandlerOptions, Provider } from "@smithy/types";
44
import { EventEmitter } from "events";
55

6-
import { requestTimeout } from "./request-timeout";
6+
import { requestTimeout as requestTimeoutFn } from "./request-timeout";
77

88
/**
99
* Represents the http options that can be passed to the xhr http client.
@@ -114,12 +114,12 @@ export class XhrHttpHandler extends EventEmitter implements HttpHandler<XhrHttpH
114114

115115
public async handle(
116116
request: HttpRequest,
117-
{ abortSignal }: HttpHandlerOptions = {}
117+
{ abortSignal, requestTimeout }: HttpHandlerOptions = {}
118118
): Promise<{ response: HttpResponse }> {
119119
if (!this.config) {
120120
this.config = await this.configProvider;
121121
}
122-
const requestTimeoutInMs = Number(this.config!.requestTimeout) | 0;
122+
const requestTimeoutInMs = Number(requestTimeout ?? this.config!.requestTimeout) | 0;
123123

124124
// if the request was already aborted, prevent doing extra work
125125
if (abortSignal?.aborted) {
@@ -214,7 +214,7 @@ export class XhrHttpHandler extends EventEmitter implements HttpHandler<XhrHttpH
214214
this.emit(EVENTS.BEFORE_XHR_SEND, xhr);
215215
xhr.send(body);
216216
}),
217-
requestTimeout(requestTimeoutInMs),
217+
requestTimeoutFn(requestTimeoutInMs),
218218
];
219219
let removeSignalEventListener = () => {};
220220
if (abortSignal) {

0 commit comments

Comments
 (0)