Skip to content

Commit

Permalink
feat: add possibility to block retries when specific http status code…
Browse files Browse the repository at this point in the history
…s happen
  • Loading branch information
RafaelJCamara committed Apr 29, 2024
1 parent 1853750 commit 8960583
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 13 deletions.
3 changes: 2 additions & 1 deletion src/@common/result.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
private constructor(public data?: T, public error: any = null) {
private constructor(public data?: T, public error?: BaseError) {
this.data = data;
this.error = error;
}
Expand Down
18 changes: 17 additions & 1 deletion src/retry/execution/retry-http-request-execution.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,7 +17,18 @@ async function retryHttpIteration(
): Promise<any> {
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;

Expand Down Expand Up @@ -53,3 +65,7 @@ const retryWithBackoff = (
backoffRetryIntervalInSeconds * 1000
)
);

const blockedStatusCodesForRetry = (retryPolicy: RetryPolicyType) =>
retryPolicy.excludeRetriesOnStatusCodes ??
DefaultRetryExcludedHttpStatusCodes;
4 changes: 4 additions & 0 deletions src/retry/models/default-retry-excluded-http-status-codes.ts
Original file line number Diff line number Diff line change
@@ -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,
];
1 change: 1 addition & 0 deletions src/retry/models/retry-policy-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export type RetryPolicyType = {
retryIntervalStrategy?: RetryIntervalStrategy;
baseRetryDelayInSeconds?: number;
timeoutPerRetryInSeconds?: number;
excludeRetriesOnStatusCodes?: number[];
};
86 changes: 76 additions & 10 deletions test/retry/retry-policy-executor.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -76,7 +77,7 @@ describe("Retry with constant backoff", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -145,7 +146,7 @@ describe("Retry with constant backoff with timeout on retry", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -219,7 +220,7 @@ describe("Retry with linear backoff", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -287,7 +288,7 @@ describe("Retry with linear backoff with timeout on retry", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -361,7 +362,7 @@ describe("Retry with linear and jitter backoff", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -429,7 +430,7 @@ describe("Retry with linear and jitter backoff with timeout on retry", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -501,7 +502,7 @@ describe("Retry with exponential backoff", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -567,7 +568,7 @@ describe("Retry with exponential backoff with timeout on retry", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -639,7 +640,7 @@ describe("Retry with exponential jitter backoff", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

Expand Down Expand Up @@ -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<ComplexObject>(
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<ComplexObject>(
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}`
);
});
}
});
2 changes: 1 addition & 1 deletion test/timeout/timeout-policy-executor.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@ describe("Timeout", () => {
},
})
);
expect(httpResult.error).toBeNull();
expect(httpResult.error).toBeUndefined();
});
});

0 comments on commit 8960583

Please sign in to comment.