diff --git a/packages/xhr-http-handler/src/xhr-http-handler.spec.ts b/packages/xhr-http-handler/src/xhr-http-handler.spec.ts index 5960824d74c80..b3dbaf8919724 100644 --- a/packages/xhr-http-handler/src/xhr-http-handler.spec.ts +++ b/packages/xhr-http-handler/src/xhr-http-handler.spec.ts @@ -14,7 +14,7 @@ class XhrMock { public static captures: any[] = []; public static DONE = 4; - private captureArgs = + protected captureArgs = (caller: string) => (...args: any[]) => { XhrMock.captures.push([caller, ...args]); @@ -244,4 +244,94 @@ describe(XhrHttpHandler.name, () => { ["getAllResponseHeaders"], ]); }); + + describe("per-request requestTimeout", () => { + it("should use per-request timeout over handler config timeout", async () => { + const handler = new XhrHttpHandler({ requestTimeout: 5000 }); + + const requestTimeoutSpy = vi.spyOn(await import("./request-timeout"), "requestTimeout"); + + const mockRequest = new HttpRequest({ + method: "GET", + hostname: "example.com", + protocol: "https:", + path: "/", + headers: {}, + }); + + class TimeoutXhrMock extends XhrMock { + send(...args: any[]) { + this.captureArgs("send")(...args); + // let it timeout + } + } + + (global as any).XMLHttpRequest = TimeoutXhrMock; + + try { + await handler.handle(mockRequest, { requestTimeout: 100 }); + } catch (error) { + // expected to timeout + } + + // verify requestTimeout function was called with per-request timeout (100), not handler timeout (5000) + expect(requestTimeoutSpy).toHaveBeenCalledWith(100); + + requestTimeoutSpy.mockRestore(); + (global as any).XMLHttpRequest = XhrMock; // restore original mock + }); + + it("should fall back to handler config timeout when per-request timeout not provided", async () => { + const handler = new XhrHttpHandler({ requestTimeout: 200 }); + + const requestTimeoutSpy = vi.spyOn(await import("./request-timeout"), "requestTimeout"); + + const mockRequest = new HttpRequest({ + method: "GET", + hostname: "example.com", + protocol: "https:", + path: "/", + headers: {}, + }); + + class TimeoutXhrMock extends XhrMock { + send(...args: any[]) { + this.captureArgs("send")(...args); + } + } + + (global as any).XMLHttpRequest = TimeoutXhrMock; + + try { + await handler.handle(mockRequest, {}); + } catch (error) {} + + expect(requestTimeoutSpy).toHaveBeenCalledWith(200); + + requestTimeoutSpy.mockRestore(); + (global as any).XMLHttpRequest = XhrMock; + }); + + it("should handle zero timeout correctly", async () => { + const handler = new XhrHttpHandler({ requestTimeout: 1000 }); + + const requestTimeoutSpy = vi.spyOn(await import("./request-timeout"), "requestTimeout"); + + const mockRequest = new HttpRequest({ + method: "GET", + hostname: "example.com", + protocol: "https:", + path: "/", + headers: {}, + }); + + try { + await handler.handle(mockRequest, { requestTimeout: 0 }); + } catch (error) {} + + expect(requestTimeoutSpy).toHaveBeenCalledWith(0); + + requestTimeoutSpy.mockRestore(); + }); + }); }); diff --git a/packages/xhr-http-handler/src/xhr-http-handler.ts b/packages/xhr-http-handler/src/xhr-http-handler.ts index 84aab7ddede4d..b410bd9483174 100644 --- a/packages/xhr-http-handler/src/xhr-http-handler.ts +++ b/packages/xhr-http-handler/src/xhr-http-handler.ts @@ -3,7 +3,7 @@ import { buildQueryString } from "@smithy/querystring-builder"; import { HttpHandlerOptions, Provider } from "@smithy/types"; import { EventEmitter } from "events"; -import { requestTimeout } from "./request-timeout"; +import { requestTimeout as requestTimeoutFn } from "./request-timeout"; /** * Represents the http options that can be passed to the xhr http client. @@ -114,12 +114,12 @@ export class XhrHttpHandler extends EventEmitter implements HttpHandler { if (!this.config) { this.config = await this.configProvider; } - const requestTimeoutInMs = Number(this.config!.requestTimeout) | 0; + const requestTimeoutInMs = Number(requestTimeout ?? this.config!.requestTimeout) | 0; // if the request was already aborted, prevent doing extra work if (abortSignal?.aborted) { @@ -214,7 +214,7 @@ export class XhrHttpHandler extends EventEmitter implements HttpHandler {}; if (abortSignal) {