Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/backend/src/api/__tests__/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,25 @@ describe('api.client', () => {
expect(errResponse.retryAfter).toBe(123);
});

it('executes a failed backend API request and includes Retry-After header RFC1123 date', async () => {
server.use(
http.get(
`https://api.clerk.test/v1/users/user_deadbeef`,
validateHeaders(() => {
return HttpResponse.json(
{ errors: [] },
{ status: 503, headers: { 'retry-after': new Date(new Date().getTime() + 60000).toUTCString() } },
);
}),
),
);

const errResponse = await apiClient.users.getUser('user_deadbeef').catch(err => err);

expect(errResponse.status).toBe(503);
expect(errResponse.retryAfter).not.toBeNaN();
});

it('executes a failed backend API request and ignores invalid Retry-After header', async () => {
server.use(
http.get(
Expand Down
12 changes: 9 additions & 3 deletions packages/backend/src/api/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,17 @@ function getRetryAfter(headers?: Headers): number | undefined {
}

const value = parseInt(retryAfter, 10);
if (isNaN(value)) {
return;
if (!isNaN(value)) {
return value;
}

const date = new Date(retryAfter);
if (!isNaN(date.getTime())) {
const value = date.getTime() - Date.now();
return value > 0 ? value : 0;
}

return value;
return;
}

function parseErrors(data: unknown): ClerkAPIError[] {
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/src/core/resources/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,19 @@ export abstract class BaseResource {
assertProductionKeysOnDev(status, errors);

const apiResponseOptions: ConstructorParameters<typeof ClerkAPIResponseError>[1] = { data: errors, status };
if (status === 429 && headers) {
if ((status === 429 || status == 503) && headers) {
const retryAfter = headers.get('retry-after');
if (retryAfter) {
const value = parseInt(retryAfter, 10);
if (!isNaN(value)) {
apiResponseOptions.retryAfter = value;
}

const date = new Date(retryAfter);
if (!isNaN(date.getTime())) {
const value = date.getTime() - Date.now();
apiResponseOptions.retryAfter = value > 0 ? value : 0;
}
}
}

Expand Down
21 changes: 21 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/Base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ describe('BaseResource', () => {
expect(errResponse.retryAfter).toBe(60);
});

it('populates retryAfter on 503 error responses', async () => {
BaseResource.clerk = {
// @ts-expect-error - We're not about to mock the entire FapiClient
getFapiClient: () => {
return {
request: vi.fn().mockResolvedValue({
payload: {},
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({ 'Retry-After': new Date(new Date().getTime() + 60000).toUTCString() }),
}),
};
},
__internal_setCountry: vi.fn(),
};
const resource = new TestResource();
const errResponse = await resource.fetch().catch(err => err);
console.dir(errResponse);
expect(errResponse.retryAfter).toBe(60);
});

it('does not populate retryAfter on invalid header', async () => {
BaseResource.clerk = {
// @ts-expect-error - We're not about to mock the entire FapiClient
Expand Down
Loading