Skip to content

Commit

Permalink
refactor: improve LogtoRequestError (#730)
Browse files Browse the repository at this point in the history
* refactor: improve `LogtoRequestError`

* refactor: fix tests
  • Loading branch information
gao-sun authored Jun 18, 2024
1 parent eb6600a commit 2f8a855
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 17 deletions.
9 changes: 9 additions & 0 deletions .changeset/wicked-wolves-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@logto/client": patch
"@logto/js": patch
---

improve `LogtoRequestError`

- Add `cause` property to `LogtoRequestError` to expose the original response.
- Make `isLogtoRequestError` more reliable by checking the instance of the error and the `name` property.
3 changes: 3 additions & 0 deletions packages/client/src/utils/requester.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('createRequester', () => {
const fetchFunction = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ code, message }),
clone: () => ({}),
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toMatchObject(new LogtoRequestError(code, message));
Expand All @@ -32,6 +33,7 @@ describe('createRequester', () => {
const fetchFunction = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ code, message, foo: 'bar' }),
clone: () => ({}),
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toMatchObject(new LogtoRequestError(code, message));
Expand Down Expand Up @@ -79,6 +81,7 @@ describe('createRequester', () => {
json: async () => {
throw new TypeError('not json content');
},
clone: () => ({}),
});
const requester = createRequester(fetchFunction);
await expect(requester('foo')).rejects.toThrowError(TypeError);
Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/utils/requester.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Requester } from '@logto/js';
import { LogtoError, LogtoRequestError, isLogtoRequestError } from '@logto/js';
import { LogtoError, LogtoRequestError, isLogtoRequestErrorJson } from '@logto/js';

/**
* A factory function that creates a requester by accepting a `fetch`-like function.
Expand All @@ -16,13 +16,13 @@ export const createRequester = (fetchFunction: typeof fetch): Requester => {
const responseJson = await response.json();
console.error(`Logto requester error: [status=${response.status}]`, responseJson);

if (!isLogtoRequestError(responseJson)) {
if (!isLogtoRequestErrorJson(responseJson)) {
throw new LogtoError('unexpected_response_error', responseJson);
}

// Expected request error from server
const { code, message } = responseJson;
throw new LogtoRequestError(code, message);
throw new LogtoRequestError(code, message, response.clone());
}

return response.json();
Expand Down
11 changes: 6 additions & 5 deletions packages/js/src/utils/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('LogtoError', () => {
const logtoError = new LogtoError(code, new OidcError(error, errorDescription));
expect(logtoError).toHaveProperty('code', code);
expect(logtoError).toHaveProperty('message', 'Missing code in the callback URI');
expect(logtoError).toHaveProperty('data', { error, errorDescription });
expect(logtoError).toHaveProperty('data', { error, errorDescription, name: 'OidcError' });
expect(logtoError.data).toBeInstanceOf(OidcError);
});
});
Expand All @@ -32,18 +32,19 @@ describe('isLogtoRequestError checks the error response from the server', () =>
expect(isLogtoRequestError({})).toBeFalsy();
});

it('should be true when the error response contains the expected properties', () => {
expect(isLogtoRequestError({ code, message })).toBeTruthy();
it('should be false for plain objects', () => {
expect(isLogtoRequestError({ code, message })).toBeFalsy();
});

it('should be true when the error response contains more than the expected properties', () => {
expect(isLogtoRequestError({ code, message, foo: 'bar' })).toBeTruthy();
it('should be true when the error response is an instance of LogtoRequestError', () => {
expect(isLogtoRequestError(new LogtoRequestError(code, message))).toBeTruthy();
});
});

describe('LogtoRequestError', () => {
test('new LogtoRequestError should contain correct properties', () => {
const logtoRequestError = new LogtoRequestError(code, message);
expect(logtoRequestError).toHaveProperty('name', 'LogtoRequestError');
expect(logtoRequestError).toHaveProperty('code', code);
expect(logtoRequestError).toHaveProperty('message', message);
});
Expand Down
34 changes: 25 additions & 9 deletions packages/js/src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,27 @@ const logtoErrorCodes = Object.freeze({
export type LogtoErrorCode = keyof typeof logtoErrorCodes;

export class LogtoError extends Error {
code: LogtoErrorCode;
data: unknown;
name = 'LogtoError';

constructor(code: LogtoErrorCode, data?: unknown) {
constructor(
public code: LogtoErrorCode,
public data?: unknown
) {
super(logtoErrorCodes[code]);
this.code = code;
this.data = data;
}
}

export const isLogtoRequestError = (data: unknown): data is { code: string; message: string } => {
export const isLogtoRequestError = (data: unknown): data is LogtoRequestError => {
if (!isArbitraryObject(data)) {
return false;
}

return data instanceof Error && data.name === 'LogtoRequestError';
};

export const isLogtoRequestErrorJson = (
data: unknown
): data is { code: string; message: string } => {
if (!isArbitraryObject(data)) {
return false;
}
Expand All @@ -35,15 +45,21 @@ export const isLogtoRequestError = (data: unknown): data is { code: string; mess
};

export class LogtoRequestError extends Error {
code: string;
name = 'LogtoRequestError';

constructor(code: string, message: string) {
constructor(
public code: string,
message: string,
/** The original response object from the server. */
public cause?: Response
) {
super(message);
this.code = code;
}
}

export class OidcError {
name = 'OidcError';

constructor(
public error: string,
public errorDescription?: string
Expand Down

0 comments on commit 2f8a855

Please sign in to comment.