Skip to content

Commit

Permalink
chore(backend,shared): Include clerkTraceId for backend api errors
Browse files Browse the repository at this point in the history
clerkTraceId is used when available and defaults to cloudflares CF Ray id
when its missing.
  • Loading branch information
Nikpolik committed Oct 20, 2023
1 parent 92727ee commit 83f200a
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 24 deletions.
7 changes: 7 additions & 0 deletions .changeset/sour-comics-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/backend': patch
'@clerk/shared': patch
---

Add clerkTraceId to ClerkBackendApiResponse and ClerkAPIResponseError to allow for better tracing and debugging API error responses.
Uses `clerk_trace_id` when available in a response and defaults to [`cf-ray` identifier](https://developers.cloudflare.com/fundamentals/reference/cloudflare-ray-id/) if missing.
42 changes: 21 additions & 21 deletions packages/backend/src/api/request.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ClerkAPIResponseError } from '@clerk/shared/error';
import type { ClerkAPIError, ClerkAPIErrorJSON } from '@clerk/types';
import deepmerge from 'deepmerge';
import snakecaseKeys from 'snakecase-keys';
Expand Down Expand Up @@ -37,6 +38,7 @@ export type ClerkBackendApiResponse<T> =
| {
data: null;
errors: ClerkAPIError[];
clerkTraceId?: string;
};

export type RequestFunction = ReturnType<typeof buildRequest>;
Expand All @@ -52,13 +54,14 @@ const withLegacyReturn =
(cb: any): LegacyRequestFunction =>
async (...args) => {
// @ts-ignore
const { data, errors, status, statusText } = await cb<T>(...args);
const { data, errors, status, statusText, clerkTraceId } = await cb<T>(...args);
if (errors === null) {
return data;
} else {
throw new ClerkAPIResponseError(statusText || '', {
data: errors,
status: status || '',
clerkTraceId,
});
}
};
Expand Down Expand Up @@ -161,6 +164,7 @@ export function buildRequest(options: CreateBackendApiOptions) {
message: err.message || 'Unexpected error',
},
],
clerkTraceId: getTraceId(err, res?.headers),
};
}

Expand All @@ -171,13 +175,29 @@ export function buildRequest(options: CreateBackendApiOptions) {
// @ts-expect-error
status: res?.status,
statusText: res?.statusText,
clerkTraceId: getTraceId(err, res?.headers),
};
}
};

return withLegacyReturn(request);
}

// Returns either clerk_trace_id if present in response json, otherwise defaults to CF-Ray header
// If the request failed before receiving a response, returns undefined
function getTraceId(data: unknown, headers?: Headers): string | undefined {
if (data && typeof data === 'object' && 'clerk_trace_id' in data && typeof data.clerk_trace_id === 'string') {
return data.clerk_trace_id;
}

const cfRay = headers?.get('cf-ray');
if (typeof cfRay === 'string') {
return cfRay;
}

return undefined;
}

function parseErrors(data: unknown): ClerkAPIError[] {
if (!!data && typeof data === 'object' && 'errors' in data) {
const errors = data.errors as ClerkAPIErrorJSON[];
Expand All @@ -197,23 +217,3 @@ function parseError(error: ClerkAPIErrorJSON): ClerkAPIError {
},
};
}

class ClerkAPIResponseError extends Error {
clerkError: true;

status: number;
message: string;

errors: ClerkAPIError[];

constructor(message: string, { data, status }: { data: ClerkAPIError[]; status: number }) {
super(message);

Object.setPrototypeOf(this, ClerkAPIResponseError.prototype);

this.clerkError = true;
this.message = message;
this.status = status;
this.errors = data;
}
}
15 changes: 12 additions & 3 deletions packages/shared/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function isNetworkError(e: any): boolean {
interface ClerkAPIResponseOptions {
data: ClerkAPIErrorJSON[];
status: number;
clerkTraceId?: string;
}

// For a comprehensive Metamask error list, please see
Expand Down Expand Up @@ -88,24 +89,32 @@ export class ClerkAPIResponseError extends Error {

status: number;
message: string;
clerkTraceId?: string;

errors: ClerkAPIError[];

constructor(message: string, { data, status }: ClerkAPIResponseOptions) {
constructor(message: string, { data, status, clerkTraceId }: ClerkAPIResponseOptions) {
super(message);

Object.setPrototypeOf(this, ClerkAPIResponseError.prototype);

this.status = status;
this.message = message;
this.clerkTraceId = clerkTraceId;
this.clerkError = true;
this.errors = parseErrors(data);
}

public toString = () => {
return `[${this.name}]\nMessage:${this.message}\nStatus:${this.status}\nSerialized errors: ${this.errors.map(e =>
JSON.stringify(e),
let message = `[${this.name}]\nMessage:${this.message}\nStatus:${this.status}\nSerialized errors: ${this.errors.map(
e => JSON.stringify(e),
)}`;

if (this.clerkTraceId) {
message += `\nClerk Trace ID: ${this.clerkTraceId}`;
}

return message;
};
}

Expand Down

0 comments on commit 83f200a

Please sign in to comment.