From 63c0ab67e831c4caf3ed5a48f8e6dd1d95cbd044 Mon Sep 17 00:00:00 2001 From: Shook Date: Mon, 29 Apr 2024 02:58:21 +0800 Subject: [PATCH 1/4] feat: support BtcAssetsApiError with context --- packages/service/src/error.ts | 20 +++++++++---- packages/service/src/service/base.ts | 36 +++++++++++++++++----- packages/service/src/types/base.ts | 12 ++++++++ packages/service/tests/Utils.test.ts | 45 +++++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 14 deletions(-) diff --git a/packages/service/src/error.ts b/packages/service/src/error.ts index e5053a28..fcb6c34f 100644 --- a/packages/service/src/error.ts +++ b/packages/service/src/error.ts @@ -1,3 +1,5 @@ +import { BtcAssetsApiContext } from './types'; + export enum ErrorCodes { UNKNOWN, @@ -20,14 +22,22 @@ export const ErrorMessages = { export class BtcAssetsApiError extends Error { public code = ErrorCodes.UNKNOWN; - constructor(code: ErrorCodes, message = ErrorMessages[code] || 'Unknown error') { + public message: string; + public context?: BtcAssetsApiContext; + + constructor(payload: { code: ErrorCodes; message?: string; context?: BtcAssetsApiContext }) { + const message = payload.message ?? ErrorMessages[payload.code] ?? ErrorMessages[ErrorCodes.UNKNOWN]; + super(message); - this.code = code; + this.message = message; + this.code = payload.code; + this.context = payload.context; Object.setPrototypeOf(this, BtcAssetsApiError.prototype); } - static withComment(code: ErrorCodes, comment?: string): BtcAssetsApiError { - const message = ErrorMessages[code] || 'Unknown error'; - return new BtcAssetsApiError(code, comment ? `${message}: ${comment}` : message); + static withComment(code: ErrorCodes, comment?: string, context?: BtcAssetsApiContext): BtcAssetsApiError { + const prefixMessage = ErrorMessages[code] ?? ErrorMessages[ErrorCodes.UNKNOWN]; + const message = comment ? `${prefixMessage}: ${comment}` : void 0; + return new BtcAssetsApiError({ code, message, context }); } } diff --git a/packages/service/src/service/base.ts b/packages/service/src/service/base.ts index 3eb38615..740e54a6 100644 --- a/packages/service/src/service/base.ts +++ b/packages/service/src/service/base.ts @@ -1,7 +1,7 @@ +import pickBy from 'lodash/pickBy'; import { isDomain } from '../utils'; import { BtcAssetsApiError, ErrorCodes } from '../error'; -import { BaseApis, BaseApiRequestOptions, BtcAssetsApiToken } from '../types'; -import { pickBy } from 'lodash'; +import { BaseApis, BaseApiRequestOptions, BtcAssetsApiToken, BtcAssetsApiContext } from '../types'; export class BtcAssetsApiBase implements BaseApis { public url: string; @@ -35,7 +35,8 @@ export class BtcAssetsApiBase implements BaseApis { const packedParams = params ? '?' + new URLSearchParams(pickBy(params, (val) => val !== undefined)).toString() : ''; const withOriginHeaders = this.origin ? { origin: this.origin } : void 0; const withAuthHeaders = requireToken && this.token ? { Authorization: `Bearer ${this.token}` } : void 0; - const res = await fetch(`${this.url}${route}${packedParams}`, { + const url = `${this.url}${route}${packedParams}`; + const res = await fetch(url, { method, headers: { ...withOriginHeaders, @@ -56,8 +57,19 @@ export class BtcAssetsApiBase implements BaseApis { // do nothing } - const status = res.status; let comment: string | undefined; + const status = res.status; + const context: BtcAssetsApiContext = { + request: { + url: `${this.url}${route}${packedParams}`, + body: tryParseBody(otherOptions.body), + params, + }, + response: { + status, + data: json ?? text, + }, + }; if (!json) { comment = text ? `(${status}) ${text}` : `${status}`; @@ -73,16 +85,16 @@ export class BtcAssetsApiBase implements BaseApis { } if (status === 200 && !json) { - throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESPONSE_DECODE_ERROR, comment); + throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESPONSE_DECODE_ERROR, comment, context); } if (status === 401) { - throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_UNAUTHORIZED, comment); + throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_UNAUTHORIZED, comment, context); } if (status === 404 && !allow404) { - throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESOURCE_NOT_FOUND, comment); + throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESOURCE_NOT_FOUND, comment, context); } if (status !== 200 && status !== 404 && !allow404) { - throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESPONSE_ERROR, comment); + throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_RESPONSE_ERROR, comment, context); } if (status !== 200) { return void 0 as T; @@ -126,3 +138,11 @@ export class BtcAssetsApiBase implements BaseApis { this.token = token.token; } } + +function tryParseBody(body: unknown): Record | undefined { + try { + return typeof body === 'string' ? JSON.parse(body) : void 0; + } catch { + return void 0; + } +} diff --git a/packages/service/src/types/base.ts b/packages/service/src/types/base.ts index 967092fd..20d80535 100644 --- a/packages/service/src/types/base.ts +++ b/packages/service/src/types/base.ts @@ -15,3 +15,15 @@ export interface BaseApiRequestOptions extends RequestInit { export interface BtcAssetsApiToken { token: string; } + +export interface BtcAssetsApiContext { + request: { + url: string; + body?: Record; + params?: Record; + }; + response: { + status: number; + data?: Record | string; + }; +} diff --git a/packages/service/tests/Utils.test.ts b/packages/service/tests/Utils.test.ts index cde161c3..9a707bc1 100644 --- a/packages/service/tests/Utils.test.ts +++ b/packages/service/tests/Utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { isDomain } from '../src'; +import { BtcAssetsApiError, ErrorCodes, isDomain } from '../src'; describe('Utils', () => { it('isDomain()', () => { @@ -12,4 +12,47 @@ describe('Utils', () => { expect(isDomain('localhost', true)).toBe(true); expect(isDomain('localhost')).toBe(false); }); + it('BtcAssetsApiError with context', () => { + try { + throw BtcAssetsApiError.withComment(ErrorCodes.ASSETS_API_INVALID_PARAM, 'param1, param2', { + request: { + url: 'https://api.com/api/v1/route', + params: { + param1: 'value1', + param2: 'value2', + }, + }, + response: { + status: 400, + data: { + error: -10, + message: 'error message about -10', + }, + }, + }); + } catch (e) { + expect(e).toBeInstanceOf(BtcAssetsApiError); + expect(e.toString()).toEqual('Error: Invalid param(s) was provided to the BtcAssetsAPI: param1, param2'); + + if (e instanceof BtcAssetsApiError) { + expect(e.code).toEqual(ErrorCodes.ASSETS_API_INVALID_PARAM); + expect(e.message).toEqual('Invalid param(s) was provided to the BtcAssetsAPI: param1, param2'); + + expect(e.context).toBeDefined(); + expect(e.context.request).toBeDefined(); + expect(e.context.request.url).toEqual('https://api.com/api/v1/route'); + expect(e.context.request.params).toEqual({ + param1: 'value1', + param2: 'value2', + }); + + expect(e.context.response).toBeDefined(); + expect(e.context.response.status).toEqual(400); + expect(e.context.response.data).toEqual({ + error: -10, + message: 'error message about -10', + }); + } + } + }); }); From c12e1f6e5e04a5126a7eadec366ccc99aef037df Mon Sep 17 00:00:00 2001 From: Shook Date: Mon, 29 Apr 2024 23:17:17 +0800 Subject: [PATCH 2/4] docs: add error handling in service lib readme --- packages/service/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/service/README.md b/packages/service/README.md index 902247b2..8281c33d 100644 --- a/packages/service/README.md +++ b/packages/service/README.md @@ -85,6 +85,28 @@ console.log(res); All available APIs in the [BtcAssetsApi](#btcassetsapi-1) section. +### Handling service errors + +You can identify the error by its `code` and `message`, or by its detailed `context`: + +```ts +import { BtcAssetsApiError, ErrorCodes } from '@rgbpp-sdk/service'; + +try { +... +} catch (e) { + if (e instanceof BtcAssetsApiError) { + // error code + console.log(e.code === ErrorCodes.ASSETS_API_UNAUTHORIZED); + // error message + console.log(e.message); + // detailed context info (if thrown from a request) + console.log(e.context?.request); + console.log(e.context?.response); + } +} +``` + ## Types ### BtcAssetsApi From 82709bb739dedbff92580a0bce3a62984ee07e5e Mon Sep 17 00:00:00 2001 From: Shook Date: Tue, 30 Apr 2024 14:17:14 +0800 Subject: [PATCH 3/4] docs: print JSON error in btc lib readme --- packages/service/README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/service/README.md b/packages/service/README.md index 8281c33d..f901995f 100644 --- a/packages/service/README.md +++ b/packages/service/README.md @@ -96,13 +96,25 @@ try { ... } catch (e) { if (e instanceof BtcAssetsApiError) { - // error code - console.log(e.code === ErrorCodes.ASSETS_API_UNAUTHORIZED); - // error message - console.log(e.message); - // detailed context info (if thrown from a request) - console.log(e.context?.request); - console.log(e.context?.response); + // check error code + console.log(e.code === ErrorCodes.ASSETS_API_UNAUTHORIZED); // true + // print the whole error + console.log(JSON.stringify(e)); + /*{ + "message": "BtcAssetsAPI unauthorized, please check your token/origin: (401) Authorization token is invalid: The token header is not a valid base64url serialized JSON.", + "code": 2, + "context": { + "request": { + "url": "https://btc-assets-api.url/bitcoin/v1/info" + }, + "response": { + "status": 401, + "data": { + "message": "Authorization token is invalid: The token header is not a valid base64url serialized JSON." + } + } + } + }*/ } } ``` From 6df8e5a18376d0334bf47871947c6fab63b95a28 Mon Sep 17 00:00:00 2001 From: Shook Date: Tue, 30 Apr 2024 18:03:30 +0800 Subject: [PATCH 4/4] refactor: simplify code --- packages/service/src/service/base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/service/src/service/base.ts b/packages/service/src/service/base.ts index 740e54a6..6e1a212f 100644 --- a/packages/service/src/service/base.ts +++ b/packages/service/src/service/base.ts @@ -61,9 +61,9 @@ export class BtcAssetsApiBase implements BaseApis { const status = res.status; const context: BtcAssetsApiContext = { request: { - url: `${this.url}${route}${packedParams}`, - body: tryParseBody(otherOptions.body), + url, params, + body: tryParseBody(otherOptions.body), }, response: { status,