diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a161f1f2..c7cc280c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -617,7 +617,7 @@ Our versioning strategy is as follows: ### 🎉 New Features & Improvements -* `[sitecore-jss]` Retry policy to handle transient network errors. Users can pass `retryStrategy` to configure custom retry config to the services. They can customize the error codes and the number of retries. It consist of two functions shouldRetry and getDelay. ([#1731](https://github.com/Sitecore/jss/pull/1731)) +* `[sitecore-jss]` Retry policy to handle transient network errors. Users can pass `retryStrategy` to configure custom retry config to the services. They can customize the error codes and the number of retries. It consist of two functions shouldRetry and getDelay. To determine the back-off time, we employ an exponential strategy with a default factor of 2.([#1731](https://github.com/Sitecore/jss/pull/1731)) ([#1733](https://github.com/Sitecore/jss/pull/1733)) ## 20.2.3 diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 12a503adea..3684ce8199 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -4,7 +4,8 @@ import { expect, use, spy } from 'chai'; import sinon from 'sinon'; import spies from 'chai-spies'; import nock from 'nock'; -import { GraphQLRequestClient } from './graphql-request-client'; +import { GraphQLRequestClient, DefaultRetryStrategy } from './graphql-request-client'; +import { ClientError } from 'graphql-request'; import debugApi from 'debug'; import debug from './debug'; @@ -405,4 +406,56 @@ describe('GraphQLRequestClient', () => { } }); }); + + describe('DefaultRetryStrategy', () => { + const mockClientError = new ClientError( + { + data: undefined, + errors: [{ message: 'GaphqlError' }], + extensions: undefined, + status: 429, + }, + { + query: 'query', + } + ); + it('should return true from shouldRetry and use default values from constructor', () => { + const retryStrategy = new DefaultRetryStrategy(); + + const shouldRetry = retryStrategy.shouldRetry(mockClientError, 1, 3); + expect(shouldRetry).to.equal(true); + }); + + it('should return false when attempt exceeds retries', () => { + const retryStrategy = new DefaultRetryStrategy({ statusCodes: [503] }); + mockClientError.response.status = 503; + + const shouldRetry = retryStrategy.shouldRetry(mockClientError, 2, 1); + expect(shouldRetry).to.equal(false); + }); + + it('should return false when retries is 0', () => { + const retryStrategy = new DefaultRetryStrategy({ statusCodes: [503] }); + mockClientError.response.status = 503; + + const shouldRetry = retryStrategy.shouldRetry(mockClientError, 1, 0); + expect(shouldRetry).to.equal(false); + }); + + it('should return delay using exponential backoff when Retry-After header is not present', () => { + const retryStrategy = new DefaultRetryStrategy(); + const delay = retryStrategy.getDelay(mockClientError, 3); + const expectedDelay = Math.pow(retryStrategy['factor'], 3 - 1) * 1000; + expect(delay).to.equal(expectedDelay); + }); + + it('should use custom exponential factor', () => { + const customFactor = 3; + const retryStrategy = new DefaultRetryStrategy({ statusCodes: [429], factor: customFactor }); + + const delay = retryStrategy.getDelay(mockClientError, 3); + const expectedDelay = Math.pow(customFactor, 3 - 1) * 1000; + expect(delay).to.equal(expectedDelay); + }); + }); }); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index d3d5596892..4b8d87252c 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -92,12 +92,13 @@ export class DefaultRetryStrategy implements RetryStrategy { private factor: number; /** - * @param {number[]} statusCodes HTTP status codes to trigger retries on - * @param {number} factor Factor by which the delay increases with each retry attempt + * @param {Object} options Configurable options for retry mechanism. + * @param {number[]} options.statusCodes HTTP status codes to trigger retries on + * @param {number} options.factor Factor by which the delay increases with each retry attempt */ - constructor(statusCodes?: number[], factor?: number) { - this.statusCodes = statusCodes || [429]; - this.factor = factor || 2; + constructor(options: { statusCodes?: number[]; factor?: number } = {}) { + this.statusCodes = options.statusCodes || [429]; + this.factor = options.factor || 2; } shouldRetry(error: ClientError, attempt: number, retries: number): boolean { @@ -113,7 +114,7 @@ export class DefaultRetryStrategy implements RetryStrategy { const rawHeaders = error.response?.headers; const delaySeconds = rawHeaders?.get('Retry-After') ? Number.parseInt(rawHeaders?.get('Retry-After'), 10) - : Math.pow(this.factor, attempt); + : Math.pow(this.factor, attempt - 1); return delaySeconds * 1000; } @@ -152,7 +153,7 @@ export class GraphQLRequestClient implements GraphQLClient { this.retries = clientConfig.retries || 0; this.retryStrategy = clientConfig.retryStrategy || - new DefaultRetryStrategy([429, 502, 503, 504, 520, 521, 522, 523, 524]); + new DefaultRetryStrategy({ statusCodes: [429, 502, 503, 504, 520, 521, 522, 523, 524] }); this.client = new Client(endpoint, { headers: this.headers, fetch: clientConfig.fetch,