diff --git a/.changeset/wicked-wolves-glow.md b/.changeset/wicked-wolves-glow.md new file mode 100644 index 00000000..551b0b6e --- /dev/null +++ b/.changeset/wicked-wolves-glow.md @@ -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. diff --git a/packages/client/src/utils/requester.test.ts b/packages/client/src/utils/requester.test.ts index a8e7b40b..16d90042 100644 --- a/packages/client/src/utils/requester.test.ts +++ b/packages/client/src/utils/requester.test.ts @@ -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)); @@ -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)); @@ -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); diff --git a/packages/client/src/utils/requester.ts b/packages/client/src/utils/requester.ts index 34f332a5..d1752481 100644 --- a/packages/client/src/utils/requester.ts +++ b/packages/client/src/utils/requester.ts @@ -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. @@ -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(); diff --git a/packages/js/src/utils/errors.test.ts b/packages/js/src/utils/errors.test.ts index b9a9742f..1a2cacf9 100644 --- a/packages/js/src/utils/errors.test.ts +++ b/packages/js/src/utils/errors.test.ts @@ -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); }); }); @@ -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); }); diff --git a/packages/js/src/utils/errors.ts b/packages/js/src/utils/errors.ts index 986b8f27..aa08fd01 100644 --- a/packages/js/src/utils/errors.ts +++ b/packages/js/src/utils/errors.ts @@ -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; } @@ -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