diff --git a/.changeset/fix-rate-limiter-throttle-detection.md b/.changeset/fix-rate-limiter-throttle-detection.md new file mode 100644 index 00000000000..14f281091f0 --- /dev/null +++ b/.changeset/fix-rate-limiter-throttle-detection.md @@ -0,0 +1,5 @@ +--- +"@smithy/util-retry": patch +--- + +fix(util-retry): detect throttling errors from RetryErrorInfo.errorType in DefaultRateLimiter diff --git a/packages/util-retry/src/DefaultRateLimiter.spec.ts b/packages/util-retry/src/DefaultRateLimiter.spec.ts index 45c5b34753d..4ffd7df86a7 100644 --- a/packages/util-retry/src/DefaultRateLimiter.spec.ts +++ b/packages/util-retry/src/DefaultRateLimiter.spec.ts @@ -119,4 +119,43 @@ describe(DefaultRateLimiter.name, () => { expect(parseFloat(rateLimiter["fillRate"].toFixed(6))).toEqual(fillRate); }); }); + + describe("throttle detection via RetryErrorInfo.errorType", () => { + it("enables the rate limiter when errorType is THROTTLING", () => { + vi.spyOn(Date, "now").mockImplementation(() => 0); + const rateLimiter = new DefaultRateLimiter(); + expect(rateLimiter["enabled"]).toBe(false); + + vi.spyOn(Date, "now").mockImplementation(() => 1000); + rateLimiter.updateClientSendingRate({ errorType: "THROTTLING" }); + expect(rateLimiter["enabled"]).toBe(true); + }); + + it("does not enable the rate limiter for non-throttling errorType", () => { + vi.spyOn(Date, "now").mockImplementation(() => 0); + const rateLimiter = new DefaultRateLimiter(); + + vi.spyOn(Date, "now").mockImplementation(() => 1000); + rateLimiter.updateClientSendingRate({ errorType: "TRANSIENT" }); + expect(rateLimiter["enabled"]).toBe(false); + }); + + it("does not call isThrottlingError when errorType is present", () => { + vi.spyOn(Date, "now").mockImplementation(() => 0); + const rateLimiter = new DefaultRateLimiter(); + + vi.spyOn(Date, "now").mockImplementation(() => 1000); + rateLimiter.updateClientSendingRate({ errorType: "THROTTLING" }); + expect(isThrottlingError).not.toHaveBeenCalled(); + }); + + it("falls back to isThrottlingError when errorType is absent", () => { + vi.spyOn(Date, "now").mockImplementation(() => 0); + const rateLimiter = new DefaultRateLimiter(); + + vi.spyOn(Date, "now").mockImplementation(() => 1000); + rateLimiter.updateClientSendingRate({}); + expect(isThrottlingError).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/util-retry/src/DefaultRateLimiter.ts b/packages/util-retry/src/DefaultRateLimiter.ts index dc345d9b1db..57d9a904180 100644 --- a/packages/util-retry/src/DefaultRateLimiter.ts +++ b/packages/util-retry/src/DefaultRateLimiter.ts @@ -97,7 +97,7 @@ export class DefaultRateLimiter implements RateLimiter { let calculatedRate: number; this.updateMeasuredRate(); - if (isThrottlingError(response)) { + if (this.isThrottlingResponse(response)) { const rateToUse = !this.enabled ? this.measuredTxRate : Math.min(this.measuredTxRate, this.fillRate); this.lastMaxRate = rateToUse; this.calculateTimeWindow(); @@ -113,6 +113,14 @@ export class DefaultRateLimiter implements RateLimiter { this.updateTokenBucketRate(newRate); } + private isThrottlingResponse(response: any): boolean { + if (response?.errorType) { + return response.errorType === "THROTTLING"; + } + return isThrottlingError(response); + } + + private calculateTimeWindow() { this.timeWindow = this.getPrecise(Math.pow((this.lastMaxRate * (1 - this.beta)) / this.scaleConstant, 1 / 3)); }