diff --git a/src/@common/result.ts b/src/@common/result.ts index d89d430..a3c52c4 100644 --- a/src/@common/result.ts +++ b/src/@common/result.ts @@ -1,8 +1,9 @@ import { RetryError } from "../retry/models/retry-error"; import { TimeoutError } from "../timeout/models/timeout-error"; +import { BaseError } from "./base-error"; export class Result { - private constructor(public data?: T, public error: any = null) { + private constructor(public data?: T, public error?: BaseError) { this.data = data; this.error = error; } diff --git a/src/retry/execution/retry-http-request-execution.ts b/src/retry/execution/retry-http-request-execution.ts index efaefd7..4d3253d 100644 --- a/src/retry/execution/retry-http-request-execution.ts +++ b/src/retry/execution/retry-http-request-execution.ts @@ -1,3 +1,4 @@ +import { DefaultRetryExcludedHttpStatusCodes } from "../models/default-retry-excluded-http-status-codes"; import { RetryIntervalStrategy } from "../models/retry-interval-options"; import { RetryPolicyType } from "../models/retry-policy-type"; import { computeRetryBackoffForStrategyInSeconds } from "../strategy/retry-backoff-strategy"; @@ -16,7 +17,18 @@ async function retryHttpIteration( ): Promise { try { return await httpRequest; - } catch (error) { + } catch (error: any) { + if ( + error.response && + error.response.status && + blockedStatusCodesForRetry(retryPolicyType).includes( + error.response.status + ) + ) { + throw new Error( + `The http status code of the response indicates that a retry shoudldn't happen. Status code received: ${error.response.status}` + ); + } if (currentAttempt <= retryPolicyType.maxNumberOfRetries) { const nextAttempt = currentAttempt + 1; @@ -53,3 +65,7 @@ const retryWithBackoff = ( backoffRetryIntervalInSeconds * 1000 ) ); + +const blockedStatusCodesForRetry = (retryPolicy: RetryPolicyType) => + retryPolicy.excludeRetriesOnStatusCodes ?? + DefaultRetryExcludedHttpStatusCodes; diff --git a/src/retry/models/default-retry-excluded-http-status-codes.ts b/src/retry/models/default-retry-excluded-http-status-codes.ts new file mode 100644 index 0000000..d8b117d --- /dev/null +++ b/src/retry/models/default-retry-excluded-http-status-codes.ts @@ -0,0 +1,4 @@ +export const DefaultRetryExcludedHttpStatusCodes: number[] = [ + 400, 401, 402, 403, 404, 405, 406, 407, 409, 410, 411, 412, 413, 414, 415, + 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, +]; diff --git a/src/retry/models/retry-policy-type.ts b/src/retry/models/retry-policy-type.ts index c2a97f5..de52721 100644 --- a/src/retry/models/retry-policy-type.ts +++ b/src/retry/models/retry-policy-type.ts @@ -5,4 +5,5 @@ export type RetryPolicyType = { retryIntervalStrategy?: RetryIntervalStrategy; baseRetryDelayInSeconds?: number; timeoutPerRetryInSeconds?: number; + excludeRetriesOnStatusCodes?: number[]; }; diff --git a/test/retry/retry-policy-executor.unit.spec.ts b/test/retry/retry-policy-executor.unit.spec.ts index 15d651c..47ad019 100644 --- a/test/retry/retry-policy-executor.unit.spec.ts +++ b/test/retry/retry-policy-executor.unit.spec.ts @@ -3,6 +3,7 @@ import { PolicyExecutorFactory } from "../../src/@common/policy-executor-factory import { RetryIntervalStrategy } from "../../src/retry/models/retry-interval-options"; import { createTimedOutRequest } from "../@common/utils/timeout-request-function"; import { ComplexObject } from "../@common/models/complex-object"; +import { DefaultRetryExcludedHttpStatusCodes } from "../../src/retry/models/default-retry-excluded-http-status-codes"; const MockAdapter = require("axios-mock-adapter"); describe("Retry with constant backoff", () => { @@ -76,7 +77,7 @@ describe("Retry with constant backoff", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -145,7 +146,7 @@ describe("Retry with constant backoff with timeout on retry", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -219,7 +220,7 @@ describe("Retry with linear backoff", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -287,7 +288,7 @@ describe("Retry with linear backoff with timeout on retry", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -361,7 +362,7 @@ describe("Retry with linear and jitter backoff", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -429,7 +430,7 @@ describe("Retry with linear and jitter backoff with timeout on retry", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -501,7 +502,7 @@ describe("Retry with exponential backoff", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -567,7 +568,7 @@ describe("Retry with exponential backoff with timeout on retry", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -639,7 +640,7 @@ describe("Retry with exponential jitter backoff", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); @@ -704,6 +705,71 @@ describe("Retry with exponential jitter backoff and timeout", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); }); + +describe("Retry with http request returning non-valid http status code for retry", () => { + let axiosMock; + + for (let httpStatusCode of DefaultRetryExcludedHttpStatusCodes) { + test("using default non-valid http status code for retry", async () => { + axiosMock = new MockAdapter(axios); + + axiosMock.onGet("/complex").reply(httpStatusCode); + + const retryPolicyExecutor = PolicyExecutorFactory.createRetryHttpExecutor( + { + maxNumberOfRetries: 3, + retryIntervalStrategy: RetryIntervalStrategy.Exponential_With_Jitter, + } + ); + + //Act + const httpResult = + await retryPolicyExecutor.ExecutePolicyAsync( + axios.get("/complex") + ); + + //Assert + expect(httpResult.data).toBeNull(); + expect(httpResult.error).not.toBeNull(); + expect(httpResult.error?.reason).toBe("retry"); + expect(httpResult.error?.message).toBe( + `The http status code of the response indicates that a retry shoudldn't happen. Status code received: ${httpStatusCode}` + ); + }); + } + + const customNonValidHttpStatusCodesForRetry = [404, 500, 503]; + + for (let httpStatusCode of customNonValidHttpStatusCodesForRetry) { + test("using custom non-valid http status code for retry", async () => { + axiosMock = new MockAdapter(axios); + + axiosMock.onGet("/complex").reply(httpStatusCode); + + const retryPolicyExecutor = PolicyExecutorFactory.createRetryHttpExecutor( + { + maxNumberOfRetries: 3, + retryIntervalStrategy: RetryIntervalStrategy.Exponential_With_Jitter, + excludeRetriesOnStatusCodes: customNonValidHttpStatusCodesForRetry, + } + ); + + //Act + const httpResult = + await retryPolicyExecutor.ExecutePolicyAsync( + axios.get("/complex") + ); + + //Assert + expect(httpResult.data).toBeNull(); + expect(httpResult.error).not.toBeNull(); + expect(httpResult.error?.reason).toBe("retry"); + expect(httpResult.error?.message).toBe( + `The http status code of the response indicates that a retry shoudldn't happen. Status code received: ${httpStatusCode}` + ); + }); + } +}); diff --git a/test/timeout/timeout-policy-executor.unit.spec.ts b/test/timeout/timeout-policy-executor.unit.spec.ts index 598c74e..f976652 100644 --- a/test/timeout/timeout-policy-executor.unit.spec.ts +++ b/test/timeout/timeout-policy-executor.unit.spec.ts @@ -69,6 +69,6 @@ describe("Timeout", () => { }, }) ); - expect(httpResult.error).toBeNull(); + expect(httpResult.error).toBeUndefined(); }); });