diff --git a/.changeset/ninety-bobcats-train.md b/.changeset/ninety-bobcats-train.md new file mode 100644 index 00000000000..55b10049983 --- /dev/null +++ b/.changeset/ninety-bobcats-train.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Handle the empty body on 204 responses from FAPI diff --git a/packages/clerk-js/src/core/__tests__/fapiClient.test.ts b/packages/clerk-js/src/core/__tests__/fapiClient.test.ts index cc93181cb5d..5e4ef4723f4 100644 --- a/packages/clerk-js/src/core/__tests__/fapiClient.test.ts +++ b/packages/clerk-js/src/core/__tests__/fapiClient.test.ts @@ -200,6 +200,23 @@ describe('request', () => { expect(Array.isArray(resp.payload)).toEqual(true); }); + it('handles the empty body on 204 response, returning null', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + Promise.resolve>({ + status: 204, + json: () => { + throw new Error('json should not be called on 204 response'); + }, + }), + ); + + const resp = await fapiClient.request({ + path: '/foo', + }); + + expect(resp.payload).toEqual(null); + }); + describe('for production instances', () => { it.todo('does not append the __clerk_db_jwt cookie value to the query string'); it.todo('does not set the __clerk_db_jwt cookie from the response Clerk-Cookie header'); diff --git a/packages/clerk-js/src/core/fapiClient.ts b/packages/clerk-js/src/core/fapiClient.ts index f996ccd8db4..37f9dedafe5 100644 --- a/packages/clerk-js/src/core/fapiClient.ts +++ b/packages/clerk-js/src/core/fapiClient.ts @@ -250,7 +250,8 @@ export function createFapiClient(options: FapiClientOptions): FapiClient { clerkNetworkError(urlStr, e); } - const json: FapiResponseJSON = await response.json(); + // 204 No Content responses do not have a body so we should not try to parse it + const json: FapiResponseJSON | null = response.status !== 204 ? await response.json() : null; const fapiResponse: FapiResponse = Object.assign(response, { payload: json }); await runAfterResponseCallbacks(requestInit, fapiResponse); return fapiResponse; diff --git a/packages/clerk-js/src/core/resources/__tests__/Client.test.ts b/packages/clerk-js/src/core/resources/__tests__/Client.test.ts index 635a85adcc0..fada67f9f5f 100644 --- a/packages/clerk-js/src/core/resources/__tests__/Client.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/Client.test.ts @@ -4,33 +4,66 @@ import { createSession, createSignIn, createSignUp, createUser } from '../../tes import { BaseResource, Client } from '../internal'; describe('Client Singleton', () => { - it('sends captcha token', async () => { - const user = createUser({ first_name: 'John', last_name: 'Doe', id: 'user_1' }); - const session = createSession({ id: 'session_1' }, user); - const clientObjectJSON: ClientJSON = { - object: 'client', - id: 'test_id', - status: 'active', - last_active_session_id: 'test_session_id', - sign_in: createSignIn({ id: 'test_sign_in_id' }, user), - sign_up: createSignUp({ id: 'test_sign_up_id' }), // This is only for testing purposes, this will never happen - sessions: [session], - created_at: jest.now() - 1000, - updated_at: jest.now(), - } as any; + describe('sendCaptchaToken', () => { + it('sends captcha token', async () => { + const user = createUser({ first_name: 'John', last_name: 'Doe', id: 'user_1' }); + const session = createSession({ id: 'session_1' }, user); + const clientObjectJSON: ClientJSON = { + object: 'client', + id: 'test_id', + status: 'active', + last_active_session_id: 'test_session_id', + sign_in: createSignIn({ id: 'test_sign_in_id' }, user), + sign_up: createSignUp({ id: 'test_sign_up_id' }), // This is only for testing purposes, this will never happen + sessions: [session], + created_at: jest.now() - 1000, + updated_at: jest.now(), + } as any; - // @ts-expect-error This is a private method that we are mocking - BaseResource._baseFetch = jest.fn(); + // @ts-expect-error This is a private method that we are mocking + BaseResource._baseFetch = jest.fn(); - const client = Client.getOrCreateInstance().fromJSON(clientObjectJSON); - await client.sendCaptchaToken({ captcha_token: 'test_captcha_token' }); - // @ts-expect-error This is a private method that we are mocking - expect(BaseResource._baseFetch).toHaveBeenCalledWith({ - method: 'POST', - path: `/client/verify`, - body: { - captcha_token: 'test_captcha_token', - }, + const client = Client.getOrCreateInstance().fromJSON(clientObjectJSON); + await client.sendCaptchaToken({ captcha_token: 'test_captcha_token' }); + // @ts-expect-error This is a private method that we are mocking + expect(BaseResource._baseFetch).toHaveBeenCalledWith({ + method: 'POST', + path: `/client/verify`, + body: { + captcha_token: 'test_captcha_token', + }, + }); + }); + + it('ignores null response payload', async () => { + const user = createUser({ first_name: 'John', last_name: 'Doe', id: 'user_1' }); + const session = createSession({ id: 'session_1' }, user); + const clientObjectJSON: ClientJSON = { + object: 'client', + id: 'test_id', + status: 'active', + last_active_session_id: 'test_session_id', + sign_in: createSignIn({ id: 'test_sign_in_id' }, user), + sign_up: createSignUp({ id: 'test_sign_up_id' }), // This is only for testing purposes, this will never happen + sessions: [session], + created_at: jest.now() - 1000, + updated_at: jest.now(), + } as any; + + // @ts-expect-error This is a private method that we are mocking + BaseResource._baseFetch = jest.fn().mockResolvedValueOnce(Promise.resolve(null)); + + const client = Client.getOrCreateInstance().fromJSON(clientObjectJSON); + await client.sendCaptchaToken({ captcha_token: 'test_captcha_token' }); + // @ts-expect-error This is a private method that we are mocking + expect(BaseResource._baseFetch).toHaveBeenCalledWith({ + method: 'POST', + path: `/client/verify`, + body: { + captcha_token: 'test_captcha_token', + }, + }); + expect(client.id).toBe('test_id'); }); });