Skip to content

Commit dd57030

Browse files
authored
chore(backend,nextjs,clerk-sdk-node): Drop legacy return response in BAPI responses (clerk#2126)
1 parent 4e844ef commit dd57030

15 files changed

Lines changed: 161 additions & 130 deletions

File tree

.changeset/mighty-pugs-knock.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@clerk/clerk-sdk-node': major
3+
'@clerk/backend': major
4+
'@clerk/nextjs': major
5+
---
6+
7+
Change the response payload of Backend API requests to return `{ data, errors }` instead of return the data and throwing on error response.
8+
Code example to keep the same behavior:
9+
```typescript
10+
import { users } from '@clerk/backend';
11+
import { ClerkAPIResponseError } from '@clerk/shared/error';
12+
13+
const { data, errors, clerkTraceId, status, statusText } = await users.getUser('user_deadbeef');
14+
if(errors){
15+
throw new ClerkAPIResponseError(statusText, { data: errors, status, clerkTraceId });
16+
}
17+
```

packages/backend/src/api/factory.test.ts

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import sinon from 'sinon';
44
import emailJson from '../fixtures/responses/email.json';
55
import userJson from '../fixtures/responses/user.json';
66
import runtime from '../runtime';
7+
import { assertErrorResponse, assertResponse } from '../util/assertResponse';
78
import { jsonError, jsonNotOk, jsonOk } from '../util/mockFetch';
89
import { createBackendApiClient } from './factory';
910

@@ -26,22 +27,17 @@ export default (QUnit: QUnit) => {
2627
fakeFetch = sinon.stub(runtime, 'fetch');
2728
fakeFetch.onCall(0).returns(jsonOk(userJson));
2829

29-
const payload = await apiClient.users.getUser('user_deadbeef');
30+
const response = await apiClient.users.getUser('user_deadbeef');
3031

31-
if (!payload) {
32-
// eslint-disable-next-line qunit/no-conditional-assertions
33-
assert.false(true, 'This assertion should never fail. We need to check for payload to make TS happy.');
34-
// eslint-disable-next-line qunit/no-early-return
35-
return;
36-
}
32+
assertResponse(assert, response);
33+
const { data: payload } = response;
3734

3835
assert.equal(payload.firstName, 'John');
3936
assert.equal(payload.lastName, 'Doe');
4037
assert.equal(payload.emailAddresses[0].emailAddress, 'john.doe@clerk.test');
4138
assert.equal(payload.phoneNumbers[0].phoneNumber, '+311-555-2368');
4239
assert.equal(payload.externalAccounts[0].emailAddress, 'john.doe@clerk.test');
4340
assert.equal(payload.publicMetadata.zodiac_sign, 'leo');
44-
// assert.equal(payload.errors, null);
4541

4642
assert.ok(
4743
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
@@ -59,22 +55,16 @@ export default (QUnit: QUnit) => {
5955
fakeFetch = sinon.stub(runtime, 'fetch');
6056
fakeFetch.onCall(0).returns(jsonOk([userJson]));
6157

62-
const payload = await apiClient.users.getUserList({ offset: 2, limit: 5 });
63-
64-
if (!payload) {
65-
// eslint-disable-next-line qunit/no-conditional-assertions
66-
assert.false(true, 'This assertion should never fail. We need to check for payload to make TS happy.');
67-
// eslint-disable-next-line qunit/no-early-return
68-
return;
69-
}
58+
const response = await apiClient.users.getUserList({ offset: 2, limit: 5 });
59+
assertResponse(assert, response);
60+
const { data: payload } = response;
7061

7162
assert.equal(payload[0].firstName, 'John');
7263
assert.equal(payload[0].lastName, 'Doe');
7364
assert.equal(payload[0].emailAddresses[0].emailAddress, 'john.doe@clerk.test');
7465
assert.equal(payload[0].phoneNumbers[0].phoneNumber, '+311-555-2368');
7566
assert.equal(payload[0].externalAccounts[0].emailAddress, 'john.doe@clerk.test');
7667
assert.equal(payload[0].publicMetadata.zodiac_sign, 'leo');
77-
// assert.equal(payload.errors, null);
7868

7969
assert.ok(
8070
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users?offset=2&limit=5', {
@@ -100,14 +90,10 @@ export default (QUnit: QUnit) => {
10090
};
10191
const requestBody =
10292
'{"from_email_name":"foobar123","email_address_id":"test@test.dev","body":"this is a test","subject":"this is a test"}';
103-
const payload = await apiClient.emails.createEmail(body);
104-
105-
if (!payload) {
106-
// eslint-disable-next-line qunit/no-conditional-assertions
107-
assert.false(true, 'This assertion should never fail. We need to check for payload to make TS happy.');
108-
// eslint-disable-next-line qunit/no-early-return
109-
return;
110-
}
93+
const response = await apiClient.emails.createEmail(body);
94+
assertResponse(assert, response);
95+
const { data: payload } = response;
96+
11197
assert.equal(JSON.stringify(payload.data), '{}');
11298
assert.equal(payload.id, 'ema_2PHa2N3bS7D6NPPQ5mpHEg0waZQ');
11399

@@ -126,15 +112,19 @@ export default (QUnit: QUnit) => {
126112

127113
test('executes a successful backend API request to create a new resource', async assert => {
128114
fakeFetch = sinon.stub(runtime, 'fetch');
129-
fakeFetch.onCall(0).returns(jsonOk([userJson]));
115+
fakeFetch.onCall(0).returns(jsonOk(userJson));
130116

131-
await apiClient.users.createUser({
117+
const response = await apiClient.users.createUser({
132118
firstName: 'John',
133119
lastName: 'Doe',
134120
publicMetadata: {
135121
star_sign: 'Leon',
136122
},
137123
});
124+
assertResponse(assert, response);
125+
const { data: payload } = response;
126+
127+
assert.equal(payload.firstName, 'John');
138128

139129
assert.ok(
140130
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users', {
@@ -161,14 +151,13 @@ export default (QUnit: QUnit) => {
161151
fakeFetch = sinon.stub(runtime, 'fetch');
162152
fakeFetch.onCall(0).returns(jsonNotOk({ errors: [mockErrorPayload], clerk_trace_id: traceId }));
163153

164-
try {
165-
await apiClient.users.getUser('user_deadbeef');
166-
} catch (e: any) {
167-
assert.equal(e.clerkTraceId, traceId);
168-
assert.true(e.clerkError);
169-
assert.equal(e.status, 422);
170-
assert.equal(e.errors[0].code, 'whatever_error');
171-
}
154+
const response = await apiClient.users.getUser('user_deadbeef');
155+
assertErrorResponse(assert, response);
156+
157+
assert.equal(response.clerkTraceId, traceId);
158+
assert.equal(response.status, 422);
159+
assert.equal(response.statusText, '422');
160+
assert.equal(response.errors[0].code, 'whatever_error');
172161

173162
assert.ok(
174163
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {
@@ -186,13 +175,12 @@ export default (QUnit: QUnit) => {
186175
fakeFetch = sinon.stub(runtime, 'fetch');
187176
fakeFetch.onCall(0).returns(jsonError({ errors: [] }));
188177

189-
try {
190-
await apiClient.users.getUser('user_deadbeef');
191-
} catch (e: any) {
192-
assert.true(e.clerkError);
193-
assert.equal(e.status, 500);
194-
assert.equal(e.clerkTraceId, 'mock_cf_ray');
195-
}
178+
const response = await apiClient.users.getUser('user_deadbeef');
179+
assertErrorResponse(assert, response);
180+
181+
assert.equal(response.status, 500);
182+
assert.equal(response.statusText, '500');
183+
assert.equal(response.clerkTraceId, 'mock_cf_ray');
196184

197185
assert.ok(
198186
fakeFetch.calledOnceWith('https://api.clerk.test/v1/users/user_deadbeef', {

packages/backend/src/api/request.ts

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ClerkAPIResponseError } from '@clerk/shared/error';
21
import type { ClerkAPIError, ClerkAPIErrorJSON } from '@clerk/types';
32
import snakecaseKeys from 'snakecase-keys';
43

@@ -36,32 +35,11 @@ export type ClerkBackendApiResponse<T> =
3635
data: null;
3736
errors: ClerkAPIError[];
3837
clerkTraceId?: string;
38+
status?: number;
39+
statusText?: string;
3940
};
4041

4142
export type RequestFunction = ReturnType<typeof buildRequest>;
42-
type LegacyRequestFunction = <T>(requestOptions: ClerkBackendApiRequestOptions) => Promise<T>;
43-
44-
/**
45-
* Switching to the { data, errors } format is a breaking change, so we will skip it for now
46-
* until we release v5 of the related SDKs.
47-
* This HOF wraps the request helper and transforms the new return to the legacy return.
48-
* TODO: Simply remove this wrapper and the ClerkAPIResponseError before the v5 release.
49-
*/
50-
const withLegacyReturn =
51-
(cb: any): LegacyRequestFunction =>
52-
async (...args) => {
53-
// @ts-ignore
54-
const { data, errors, status, statusText, clerkTraceId } = await cb<T>(...args);
55-
if (errors === null) {
56-
return data;
57-
} else {
58-
throw new ClerkAPIResponseError(statusText || '', {
59-
data: errors,
60-
status: status || '',
61-
clerkTraceId,
62-
});
63-
}
64-
};
6543

6644
type BuildRequestOptions = {
6745
/* Secret Key */
@@ -73,9 +51,8 @@ type BuildRequestOptions = {
7351
/* Library/SDK name */
7452
userAgent?: string;
7553
};
76-
7754
export function buildRequest(options: BuildRequestOptions) {
78-
const request = async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
55+
return async <T>(requestOptions: ClerkBackendApiRequestOptions): Promise<ClerkBackendApiResponse<T>> => {
7956
const { secretKey, apiUrl = API_URL, apiVersion = API_VERSION, userAgent = USER_AGENT } = options;
8057
const { path, method, queryParams, headerParams, bodyParams, formData } = requestOptions;
8158

@@ -133,7 +110,13 @@ export function buildRequest(options: BuildRequestOptions) {
133110
const data = await (isJSONResponse ? res.json() : res.text());
134111

135112
if (!res.ok) {
136-
throw data;
113+
return {
114+
data: null,
115+
errors: data?.errors || data,
116+
status: res?.status,
117+
statusText: res?.statusText,
118+
clerkTraceId: getTraceId(data, res?.headers),
119+
};
137120
}
138121

139122
return {
@@ -157,16 +140,12 @@ export function buildRequest(options: BuildRequestOptions) {
157140
return {
158141
data: null,
159142
errors: parseErrors(err),
160-
// TODO: To be removed with withLegacyReturn
161-
// @ts-expect-error
162143
status: res?.status,
163144
statusText: res?.statusText,
164145
clerkTraceId: getTraceId(err, res?.headers),
165146
};
166147
}
167148
};
168-
169-
return withLegacyReturn(request);
170149
}
171150

172151
// Returns either clerk_trace_id if present in response json, otherwise defaults to CF-Ray header

packages/backend/src/tokens/authStatus.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,9 @@ export async function signedIn<T extends AuthStatusOptionsType>(
149149
loadOrganization && orgId ? organizations.getOrganization({ organizationId: orgId }) : Promise.resolve(undefined),
150150
]);
151151

152-
const session = sessionResp;
153-
const user = userResp;
154-
const organization = organizationResp;
155-
// const session = sessionResp && !sessionResp.errors ? sessionResp.data : undefined;
156-
// const user = userResp && !userResp.errors ? userResp.data : undefined;
157-
// const organization = organizationResp && !organizationResp.errors ? organizationResp.data : undefined;
152+
const session = sessionResp && !sessionResp.errors ? sessionResp.data : undefined;
153+
const user = userResp && !userResp.errors ? userResp.data : undefined;
154+
const organization = organizationResp && !organizationResp.errors ? organizationResp.data : undefined;
158155

159156
const authObject = signedInAuthObject(
160157
sessionClaims,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type ApiResponse<T> = { data: T | null; errors: null | any[] };
2+
type SuccessApiResponse<T> = { data: T; errors: null };
3+
type ErrorApiResponse = { data: null; errors: any[]; clerkTraceId: string; status: number; statusText: string };
4+
export function assertResponse<T>(assert: Assert, resp: ApiResponse<T>): asserts resp is SuccessApiResponse<T> {
5+
assert.equal(resp.errors, null);
6+
}
7+
export function assertErrorResponse<T>(assert: Assert, resp: ApiResponse<T>): asserts resp is ErrorApiResponse {
8+
assert.notEqual(resp.errors, null);
9+
}

packages/backend/src/util/mockFetch.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export function jsonOk(body: unknown, status = 200) {
55
const mockResponse = {
66
ok: true,
77
status,
8+
statusText: status.toString(),
89
headers: { get: mockHeadersGet },
910
json() {
1011
return Promise.resolve(body);
@@ -19,6 +20,7 @@ export function jsonNotOk(body: unknown) {
1920
const mockResponse = {
2021
ok: false,
2122
status: 422,
23+
statusText: 422,
2224
headers: { get: mockHeadersGet },
2325
json() {
2426
return Promise.resolve(body);
@@ -32,7 +34,8 @@ export function jsonError(body: unknown, status = 500) {
3234
// Mock response object that satisfies the window.Response interface
3335
const mockResponse = {
3436
ok: false,
35-
status: status,
37+
status,
38+
statusText: status.toString(),
3639
headers: { get: mockHeadersGet },
3740
json() {
3841
return Promise.resolve(body);

packages/nextjs/src/app-router/server/currentUser.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ import { auth } from './auth';
55

66
export async function currentUser(): Promise<User | null> {
77
const { userId } = auth();
8-
return userId ? clerkClient.users.getUser(userId) : null;
8+
if (!userId) return null;
9+
10+
const { data, errors } = await clerkClient.users.getUser(userId);
11+
if (errors) return null;
12+
13+
return data;
914
}

packages/sdk-node/examples/express/src/runtime-keys-middleware.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ app.use(clerk.expressWithAuth());
2121
app.get('/', async (req: WithAuthProp<Request>, res: Response) => {
2222
const { userId, debug } = req.auth;
2323
console.log(debug());
24-
const user = userId ? await clerk.users.getUser(userId) : null;
25-
res.json({ auth: req.auth, user });
24+
if (!userId) return res.json({ auth: req.auth, user: null });
25+
26+
const { data, errors } = await clerk.users.getUser(userId);
27+
if (errors) return res.json({ auth: req.auth, user: null });
28+
29+
return res.json({ auth: req.auth, user: data });;
2630
});
2731

2832
// @ts-ignore
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import { organizations, users } from '@clerk/clerk-sdk-node';
22

33
console.log('Get user to create organization');
4-
const [creator] = await users.getUserList();
4+
const { data, errors } = await users.getUserList();
5+
if (errors) {
6+
throw new Error(errors);
7+
}
8+
9+
const creator = data[0];
510

611
console.log('Create organization');
7-
const organization = await organizations.createOrganization({
12+
const { data: organization } = await organizations.createOrganization({
813
name: 'test-organization',
914
createdBy: creator.id,
1015
});
1116
console.log(organization);
1217

1318
console.log('Update organization metadata');
14-
const updatedOrganizationMetadata =
15-
await organizations.updateOrganizationMetadata(organization.id, {
19+
const { data: updatedOrganizationMetadata, errors: uomErrors } = await organizations.updateOrganizationMetadata(
20+
organization.id,
21+
{
1622
publicMetadata: { test: 1 },
17-
});
23+
},
24+
);
25+
if (uomErrors) {
26+
throw new Error(uomErrors);
27+
}
28+
1829
console.log(updatedOrganizationMetadata);

0 commit comments

Comments
 (0)