Skip to content
This repository was archived by the owner on May 27, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,40 @@ 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) {
// 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."
}
}
}
}*/
}
}
```

## Types

### BtcAssetsApi
Expand Down
20 changes: 15 additions & 5 deletions packages/service/src/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BtcAssetsApiContext } from './types';

export enum ErrorCodes {
UNKNOWN,

Expand All @@ -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 });
}
}
36 changes: 28 additions & 8 deletions packages/service/src/service/base.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -35,7 +35,8 @@ export class BtcAssetsApiBase implements BaseApis {
const packedParams = params ? '?' + new URLSearchParams(pickBy(params, (val) => val !== undefined)).toString() : '';
Copy link
Collaborator

@duanyytop duanyytop Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is val => !!val more simple and clear?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think val !== undefined is better, if val is 0, it will be filtered out here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The type of val is unknown

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,
Expand All @@ -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,
params,
body: tryParseBody(otherOptions.body),
},
response: {
status,
data: json ?? text,
},
};

if (!json) {
comment = text ? `(${status}) ${text}` : `${status}`;
Expand All @@ -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;
Expand Down Expand Up @@ -126,3 +138,11 @@ export class BtcAssetsApiBase implements BaseApis {
this.token = token.token;
}
}

function tryParseBody(body: unknown): Record<string, any> | undefined {
try {
return typeof body === 'string' ? JSON.parse(body) : void 0;
} catch {
return void 0;
Copy link
Contributor

@ahonn ahonn Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid using void 0, it is the same as undefined, but not as easy to understand.

Some libraries/packages use void 0 because undefined can be shadowed by local variables, and void 0 is shorter and can save a few bytes. You may see underscore or lodash used this way, but it doesn't mean we need it.
For modern JavaScript/TypeScript, this is a problem that does not require concern. Local variable shadowing cannot occur in TypeScript, and bundle size should be left to webpack/esbuild or other tools, rather than written as void 0

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that undefined is better than void 0

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kinda a coding style habit for me, I use it because it's easy to type.
I'll replace all void 0 to undefined in another pull request.

}
}
12 changes: 12 additions & 0 deletions packages/service/src/types/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ export interface BaseApiRequestOptions extends RequestInit {
export interface BtcAssetsApiToken {
token: string;
}

export interface BtcAssetsApiContext {
request: {
url: string;
body?: Record<string, any>;
params?: Record<string, any>;
};
response: {
status: number;
data?: Record<string, any> | string;
};
}
45 changes: 44 additions & 1 deletion packages/service/tests/Utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { isDomain } from '../src';
import { BtcAssetsApiError, ErrorCodes, isDomain } from '../src';

describe('Utils', () => {
it('isDomain()', () => {
Expand All @@ -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',
});
}
}
});
});