Skip to content
5 changes: 5 additions & 0 deletions .changeset/ninety-bobcats-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Handle the empty body on 204 responses from FAPI
17 changes: 17 additions & 0 deletions packages/clerk-js/src/core/__tests__/fapiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecursivePartial<Response>>({
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');
Expand Down
3 changes: 2 additions & 1 deletion packages/clerk-js/src/core/fapiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ export function createFapiClient(options: FapiClientOptions): FapiClient {
clerkNetworkError(urlStr, e);
}

const json: FapiResponseJSON<T> = await response.json();
// 204 No Content responses do not have a body so we should not try to parse it
const json: FapiResponseJSON<T> | null = response.status !== 204 ? await response.json() : null;
const fapiResponse: FapiResponse<T> = Object.assign(response, { payload: json });
await runAfterResponseCallbacks(requestInit, fapiResponse);
return fapiResponse;
Expand Down
83 changes: 58 additions & 25 deletions packages/clerk-js/src/core/resources/__tests__/Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down