Skip to content

Commit

Permalink
feat(js-sdk): support retry-after header
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanpq committed Mar 4, 2025
1 parent 29acb29 commit fb14410
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 12 deletions.
81 changes: 71 additions & 10 deletions config/clients/js/template/common.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,47 @@ function isAxiosError(err: any): boolean {
function randomTime(loopCount: number, minWaitInMs: number): number {
const min = Math.ceil(2 ** loopCount * minWaitInMs);
const max = Math.ceil(2 ** (loopCount + 1) * minWaitInMs);
return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive
const calculatedTime = Math.floor(Math.random() * (max - min) + min);
return Math.min(calculatedTime, 120000);
}
function parseRetryAfterHeader(headers: Record<string, string | string[] | undefined>): number | undefined {
const retryAfter = headers["retry-after"] || headers["Retry-After"];
if (!retryAfter) {
return undefined;
}

const retryAfterValue = Array.isArray(retryAfter) ? retryAfter[0] : retryAfter;

if (!retryAfterValue) {
return undefined;
}

// Try to parse as integer (seconds)
const secondsValue = parseInt(retryAfterValue, 10);
if (!isNaN(secondsValue)) {
const msValue = secondsValue * 1000;
if (msValue >= 1000 && msValue <= 1800000) {
return msValue;
}
return undefined;
}

try {
const dateValue = new Date(retryAfterValue);
const now = new Date();
const delayMs = dateValue.getTime() - now.getTime();
if (delayMs >= 1000 && delayMs <= 1800000) {
return delayMs;
}
} catch (e) {
// Invalid date format
}

return undefined;
}


interface WrappedAxiosResponse<R> {
response?: AxiosResponse<R>;
Expand Down Expand Up @@ -164,18 +203,40 @@ export async function attemptHttpRequest<B, R>(
throw new FgaApiAuthenticationError(err);
} else if (status === 404) {
throw new FgaApiNotFoundError(err);
} else if (status === 429 || status >= 500) {
if (iterationCount >= config.maxRetry) {
} else if (status === 429 || (status >= 500 && status !== 501)) {
if (iterationCount >= config.maxRetry) {
// We have reached the max retry limit
// Thus, we have no choice but to throw
if (status === 429) {
throw new FgaApiRateLimitExceededError(err);
} else {
throw new FgaApiInternalError(err);
}
if (status === 429) {
throw new FgaApiRateLimitExceededError(err);
} else {
throw new FgaApiInternalError(err);
}
await new Promise(r => setTimeout(r, randomTime(iterationCount, config.minWaitInMs)));
} else {
}

let retryDelayMs: number | undefined;

// Check for Retry-After header
if (err.response?.headers) {
retryDelayMs = parseRetryAfterHeader(err.response.headers);
}

// If Retry-After header isn't valid or not present, use exponential backoff
if (retryDelayMs === undefined) {
// For 429s, we always retry with exponential backoff
if (status === 429) {
retryDelayMs = randomTime(iterationCount, config.minWaitInMs);
} else if (status >= 500 && status !== 501) {
// For 5xx (except 501), we only retry if Retry-After was present
// Since we already checked and it wasn't, we throw
throw new FgaApiInternalError(err);
}
}
if (retryDelayMs !== undefined) {
await new Promise(r => setTimeout(r, retryDelayMs));
}
} else {
throw new FgaApiError(err);
}
}
Expand Down
3 changes: 2 additions & 1 deletion config/clients/js/template/tests/helpers/nocks.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ export const getNocks = ((nock: typeof Nock) => ({
accessToken = "test-token",
expiresIn = 300,
statusCode = 200,
headers = {},
) => {
return nock(`https://${apiTokenIssuer}`, { reqheaders: { "Content-Type": "application/x-www-form-urlencoded"} })
.post("/oauth/token")
.reply(statusCode, {
access_token: accessToken,
expires_in: expiresIn,
});
}, headers);
},
listStores: (
basePath = defaultConfiguration.getBasePath(),
Expand Down
6 changes: 5 additions & 1 deletion config/clients/js/template/tests/index.test.ts.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ describe("{{appTitleCaseName}} SDK", function () {
});

it("should retry a failed attempt to request to exchange the credentials", async () => {
const scope1 = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token", 300, 500);
const scope1 = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token", 300, 500, {
'Retry-After': '1' // Add Retry-After header
});
const scope2 = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER);
nocks.readAuthorizationModels(baseConfig.storeId!);

Expand Down Expand Up @@ -499,6 +501,8 @@ describe("{{appTitleCaseName}} SDK", function () {
.reply(500, {
code: "internal_error",
message: "nock error",
}, {
'Retry-After': '1'
});

nock(basePath)
Expand Down

0 comments on commit fb14410

Please sign in to comment.