Skip to content
Open
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
32 changes: 28 additions & 4 deletions src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,13 @@ describe('Client', () => {

await expect(
client.request(HttpMethod.get, '/products/:count', {}),
).rejects.toThrow(new Error('Internal Server Error'));
).rejects.toThrow(
new Error('Internal Server Error', {
cause: new Error('Request failed with status code 500', {
cause: new Error('Request failed with status code 500'),
}),
}),
);
});

test('handles a timeout', async () => {
Expand All @@ -247,7 +253,13 @@ describe('Client', () => {

await expect(
client.request(HttpMethod.get, '/products/:count', {}),
).rejects.toThrow(new Error('timeout of 0ms exceeded'));
).rejects.toThrow(
new Error('timeout of 0ms exceeded', {
cause: new Error('timeout of 0ms exceeded', {
cause: new Error('timeout of 0ms exceeded'),
}),
}),
);
});
}); // describe: #request

Expand All @@ -260,7 +272,13 @@ describe('Client', () => {

await expect(
client.request(HttpMethod.get, '/products/:count', {}),
).rejects.toThrow(new Error('timeout of 0ms exceeded'));
).rejects.toThrow(
new Error('timeout of 0ms exceeded', {
cause: new Error('timeout of 0ms exceeded', {
cause: new Error('timeout of 0ms exceeded'),
}),
}),
);
});

test('handle retries option', async () => {
Expand Down Expand Up @@ -301,7 +319,13 @@ describe('Client', () => {

await expect(
client.request(HttpMethod.get, '/categories/:count', {}),
).rejects.toThrow(new Error('timeout of 0ms exceeded'));
).rejects.toThrow(
new Error('timeout of 0ms exceeded', {
cause: new Error('timeout of 0ms exceeded', {
cause: new Error('timeout of 0ms exceeded'),
}),
}),
);
});

test('handle return error code without retries', async () => {
Expand Down
59 changes: 50 additions & 9 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as axios from 'axios';
import axios from 'axios';
import * as retry from 'retry';
import { CookieJar } from 'tough-cookie';
import { HttpCookieAgent, HttpsCookieAgent } from 'http-cookie-agent/http';
Expand Down Expand Up @@ -63,7 +63,6 @@ const DEFAULT_OPTIONS: Readonly<ClientOptions> = Object.freeze({
});

class ApiError extends Error {
message: string;
code?: string;
status?: number;
headers: HttpHeaders;
Expand All @@ -73,10 +72,10 @@ class ApiError extends Error {
code?: string,
status?: number,
headers: HttpHeaders = {},
cause?: unknown,
) {
super();
super(message, cause ? { cause } : undefined);

this.message = message;
this.code = code;
this.status = status;
this.headers = headers;
Expand Down Expand Up @@ -305,7 +304,10 @@ export class Client {
// Prepare url and data for request
const requestParams = transformRequest(method, url, data, headers);

return new Promise((resolve, reject) => {
const executor = (
resolve: (value: T) => void,
reject: (reason: unknown) => void,
) => {
const { retries } = this.options;

const operation = retry.operation({
Expand All @@ -316,6 +318,9 @@ export class Client {
randomize: false,
});

// Remember stacktrace
const stacktrace = captureStackTrace(executor);

operation.attempt(async () => {
if (this.httpClient === null) {
return reject(new Error('Swell API client not initialized'));
Expand All @@ -341,13 +346,15 @@ export class Client {
) {
return;
}
reject(transformError(error));
reject(transformError(error, stacktrace));
} finally {
// Decrement active request counter
clientWrapper.activeRequests--;
}
});
});
};

return new Promise(executor);
}

/**
Expand Down Expand Up @@ -426,13 +433,40 @@ function isError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error;
}

// eslint-disable-next-line @typescript-eslint/ban-types
function captureStackTrace(constructorOpt?: Function): string {
if (typeof Error.captureStackTrace === 'function') {
const error = { stack: '' };
Error.captureStackTrace(error, constructorOpt || captureStackTrace);
// Remove first line with empty "Error"
return error.stack.slice(error.stack.indexOf('\n') + 1);
}

// Fallback to extract stacktrace from Error
let stack = new Error('').stack || '';
let pos = stack.indexOf('\n');

if (pos !== -1) {
// Cut 2 lines from the stacktrace
pos = stack.indexOf('\n', pos + 1);
pos = stack.indexOf('\n', pos + 1);

if (pos !== -1) {
stack = stack.slice(pos + 1);
}
}

return stack;
}

/**
* Transforms the error response.
*
* @param error The Error object
* @param stacktrace The stacktrace of the request
* @return {ApiError}
*/
function transformError(error: unknown): ApiError {
function transformError(error: unknown, stacktrace?: string): ApiError {
let code,
message = '',
status,
Expand Down Expand Up @@ -464,12 +498,19 @@ function transformError(error: unknown): ApiError {
message = error.message;
}

return new ApiError(
const apiError = new ApiError(
message,
typeof code === 'string' ? code.toUpperCase().replace(/ /g, '_') : 'ERROR',
status,
headers,
error,
);

if (stacktrace) {
apiError.stack = apiError.toString() + '\n' + stacktrace;
}

return apiError;
}

function normalizeHeaders(
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"lib": ["es2020"],
"lib": ["es2022"],
"module": "node16",
"target": "es2020",
"strict": true,
Expand Down