Skip to content

Commit 1500de9

Browse files
authored
feat: Adds new HttpException class which will be return instead of th… (#308)
* feat: Adds new HttpException class which will be return instead of the plain object in case of error. * feat: Adds new HttpException class which will be return instead of the plain object in case of error. * feat: Adds new HttpException class which will be return instead of the plain object in case of error. * feat: Adds new HttpException class which will be return instead of the plain object in case of error. * feat: Adds new HttpException class which will be return instead of the plain object in case of error.
1 parent 4a17531 commit 1500de9

File tree

6 files changed

+204
-22
lines changed

6 files changed

+204
-22
lines changed

README.md

+29
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Usability, consistency, and performance are key focuses of jira.js, and it also
2424
- [Basic](#basic-authentication)
2525
- [OAuth 2.0](#oauth-20)
2626
- [Personal access token](#personal-access-token)
27+
- [Error handling](#error-handling)
2728
- [Example and using algorithm](#example-and-using-algorithm)
2829
- [Decreasing Webpack bundle size](#decreasing-webpack-bundle-size)
2930
- [Take a look at our other products](#take-a-look-at-our-other-products)
@@ -123,6 +124,34 @@ const client = new Version3Client({
123124
});
124125
```
125126

127+
#### Error handling
128+
Starting from version 4.0.0, the library has a new error handling system.
129+
Now, all errors are instances of
130+
- the `HttpException` class in case the Axios has response from the server;
131+
- the `AxiosError` class in case something went wrong before sending the request.
132+
133+
The `HttpException` class tries to parse different sorts of responses from the server to provide a unified error class.
134+
135+
If the original error is required, you can get it from the `cause` property of the `HttpException` class.
136+
137+
```typescript
138+
try {
139+
const users = await this.client.userSearch.findUsers({ query: email });
140+
// ...
141+
} catch (error: uknown) {
142+
if (error instanceof HttpException) {
143+
console.log(error.message);
144+
console.log(error.cause); // original error (AxiosError | Error)
145+
console.log(error.cause.response?.headers); // headers from the server
146+
} else if (error instanceof AxiosError) {
147+
console.log(error.message);
148+
console.log(error.code); // error code, for instance AxiosError.ETIMEDOUT
149+
} else {
150+
console.log(error);
151+
}
152+
}
153+
````
154+
126155
#### Example and using algorithm
127156

128157
1. Example

src/callback.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { AxiosError } from 'axios';
1+
import { Config } from './config';
22

3-
export type Callback<T> = (err: AxiosError | null, data?: T) => void;
3+
export type Callback<T> = (err: Config.Error | null, data?: T) => void;

src/clients/baseClient.ts

+33-19
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
1+
import axios, { AxiosInstance, AxiosResponse } from 'axios';
22
import type { Callback } from '../callback';
33
import type { Client } from './client';
44
import type { Config } from '../config';
55
import { getAuthenticationToken } from '../services/authenticationService';
66
import type { RequestConfig } from '../requestConfig';
7+
import { HttpException, isObject } from './httpException';
78

89
const STRICT_GDPR_FLAG = 'x-atlassian-force-account-id';
910
const ATLASSIAN_TOKEN_CHECK_FLAG = 'X-Atlassian-Token';
@@ -91,7 +92,7 @@ export class BaseClient implements Client {
9192
const response = await this.sendRequestFullResponse<T>(requestConfig);
9293

9394
return this.handleSuccessResponse(response.data, callback);
94-
} catch (e: any) {
95+
} catch (e: unknown) {
9596
return this.handleFailedResponse(e, callback);
9697
}
9798
}
@@ -119,11 +120,11 @@ export class BaseClient implements Client {
119120
return responseHandler(response);
120121
}
121122

122-
handleFailedResponse<T>(e: Error, callback?: Callback<T> | never): void {
123-
const err = axios.isAxiosError(e) && e.response ? this.buildErrorHandlingResponse(e) : e;
123+
handleFailedResponse<T>(e: unknown, callback?: Callback<T> | never): void {
124+
const err = this.buildErrorHandlingResponse(e);
124125

125126
const callbackErrorHandler = callback && ((error: Config.Error) => callback(error));
126-
const defaultErrorHandler = (error: Error) => {
127+
const defaultErrorHandler = (error: Config.Error) => {
127128
throw error;
128129
};
129130

@@ -134,20 +135,33 @@ export class BaseClient implements Client {
134135
return errorHandler(err);
135136
}
136137

137-
private buildErrorHandlingResponse(error: AxiosError<any>) {
138-
const headers = error.response?.headers ?? {};
139-
const responseData = error.response?.data ?? {};
140-
const data = typeof responseData === 'object' ? responseData : { data: responseData };
138+
private buildErrorHandlingResponse(e: unknown): Config.Error {
139+
if (axios.isAxiosError(e) && e.response) {
140+
return new HttpException(
141+
{
142+
code: e.code,
143+
message: e.message,
144+
data: e.response.data,
145+
status: e.response?.status,
146+
statusText: e.response?.statusText,
147+
},
148+
e.response.status,
149+
{ cause: e },
150+
);
151+
}
141152

142-
return {
143-
code: error.code,
144-
headers: this.removeUndefinedProperties({
145-
[RETRY_AFTER]: headers[RETRY_AFTER],
146-
[RATE_LIMIT_RESET]: headers[RATE_LIMIT_RESET],
147-
}),
148-
status: error.response?.status,
149-
statusText: error.response?.statusText,
150-
...data,
151-
};
153+
if (axios.isAxiosError(e)) {
154+
return e;
155+
}
156+
157+
if (isObject(e) && isObject((e as Record<string, any>).response)) {
158+
return new HttpException((e as Record<string, any>).response);
159+
}
160+
161+
if (e instanceof Error) {
162+
return new HttpException(e);
163+
}
164+
165+
return new HttpException('Unknown error occurred.', 500, { cause: e });
152166
}
153167
}

src/clients/httpException.ts

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
export const isUndefined = (obj: any): obj is undefined => typeof obj === 'undefined';
2+
3+
export const isNil = (val: any): val is null | undefined => isUndefined(val) || val === null;
4+
5+
export const isObject = (fn: any): fn is object => !isNil(fn) && typeof fn === 'object';
6+
7+
export const isString = (val: any): val is string => typeof val === 'string';
8+
9+
export const isNumber = (val: any): val is number => typeof val === 'number';
10+
11+
export interface HttpExceptionOptions {
12+
/** Original cause of the error */
13+
cause?: unknown;
14+
description?: string;
15+
}
16+
17+
export const DEFAULT_EXCEPTION_STATUS = 500;
18+
export const DEFAULT_EXCEPTION_MESSAGE = 'Something went wrong';
19+
export const DEFAULT_EXCEPTION_CODE = 'INTERNAL_SERVER_ERROR';
20+
export const DEFAULT_EXCEPTION_STATUS_TEXT = 'Internal server error';
21+
22+
/** Defines the base HTTP exception, which is handled by the default Exceptions Handler. */
23+
export class HttpException extends Error {
24+
/**
25+
* Instantiate a plain HTTP Exception.
26+
*
27+
* @example
28+
* throw new HttpException('message', HttpStatus.BAD_REQUEST);
29+
* throw new HttpException('custom message', HttpStatus.BAD_REQUEST, {
30+
* cause: new Error('Cause Error'),
31+
* });
32+
*
33+
* @param response String, object describing the error condition or the error cause.
34+
* @param status HTTP response status code.
35+
* @param options An object used to add an error cause. Configures error chaining support
36+
* @usageNotes
37+
* The constructor arguments define the response and the HTTP response status code.
38+
* - The `response` argument (required) defines the JSON response body. alternatively, it can also be
39+
* an error object that is used to define an error [cause](https://nodejs.org/en/blog/release/v16.9.0/#error-cause).
40+
* - The `status` argument (optional) defines the HTTP Status Code.
41+
* - The `options` argument (optional) defines additional error options. Currently, it supports the `cause` attribute,
42+
* and can be used as an alternative way to specify the error cause: `const error = new HttpException('description', 400, { cause: new Error() });`
43+
*
44+
* By default, the JSON response body contains two properties:
45+
* - `statusCode`: the Http Status Code.
46+
* - `message`: a short description of the HTTP error by default; override this
47+
* by supplying a string in the `response` parameter.
48+
*
49+
* The `status` argument is required, and should be a valid HTTP status code.
50+
* Best practice is to use the `HttpStatus` enum imported from `nestjs/common`.
51+
* @see https://nodejs.org/en/blog/release/v16.9.0/#error-cause
52+
* @see https://github.com/microsoft/TypeScript/issues/45167
53+
*/
54+
constructor(
55+
public readonly response: string | Record<string, any>,
56+
status?: number,
57+
options?: HttpExceptionOptions,
58+
) {
59+
super();
60+
61+
this.name = this.initName();
62+
this.cause = this.initCause(response, options);
63+
this.code = this.initCode(response);
64+
this.message = this.initMessage(response);
65+
this.status = this.initStatus(response, status);
66+
this.statusText = this.initStatusText(response, this.status);
67+
}
68+
69+
public readonly cause?: unknown;
70+
public readonly code?: string;
71+
public readonly status: number;
72+
public readonly statusText?: string;
73+
74+
protected initMessage(response: string | Record<string, any>) {
75+
if (isString(response)) {
76+
return response;
77+
}
78+
79+
if (isObject(response) && isString((response as Record<string, any>).message)) {
80+
return (response as Record<string, any>).message;
81+
}
82+
83+
if (this.constructor) {
84+
return this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g)?.join(' ') ?? 'Error';
85+
}
86+
87+
return DEFAULT_EXCEPTION_MESSAGE;
88+
}
89+
90+
protected initCause(response: string | Record<string, any>, options?: HttpExceptionOptions): unknown {
91+
if (options?.cause) {
92+
return options.cause;
93+
}
94+
95+
if (isObject(response) && isObject((response as Record<string, any>).cause)) {
96+
return (response as Record<string, any>).cause;
97+
}
98+
99+
return undefined;
100+
}
101+
102+
protected initCode(response: string | Record<string, any>): string {
103+
if (isObject(response) && isString((response as Record<string, any>).code)) {
104+
return (response as Record<string, any>).code;
105+
}
106+
107+
return DEFAULT_EXCEPTION_CODE;
108+
}
109+
110+
protected initName(): string {
111+
return this.constructor.name;
112+
}
113+
114+
protected initStatus(response: string | Record<string, any>, status?: number): number {
115+
if (status) {
116+
return status;
117+
}
118+
119+
if (isObject(response) && isNumber((response as Record<string, any>).status)) {
120+
return (response as Record<string, any>).status;
121+
}
122+
123+
if (isObject(response) && isNumber((response as Record<string, any>).statusCode)) {
124+
return (response as Record<string, any>).statusCode;
125+
}
126+
127+
return DEFAULT_EXCEPTION_STATUS;
128+
}
129+
130+
protected initStatusText(response: string | Record<string, any>, status?: number): string | undefined {
131+
if (isObject(response) && isString((response as Record<string, any>).statusText)) {
132+
return (response as Record<string, any>).statusText;
133+
}
134+
135+
return status ? undefined : DEFAULT_EXCEPTION_STATUS_TEXT;
136+
}
137+
}

src/clients/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './baseClient';
22
export * from './client';
3+
export * from './httpException';
34

45
export { AgileClient, AgileModels, AgileParameters } from '../agile';
56

src/config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AxiosError } from 'axios';
22
import { RequestConfig } from './requestConfig';
33
import { UtilityTypes } from './utilityTypes';
4+
import { HttpException } from './clients';
45

56
export interface Config {
67
host: string;
@@ -14,7 +15,7 @@ export interface Config {
1415

1516
export namespace Config {
1617
export type BaseRequestConfig = RequestConfig;
17-
export type Error = AxiosError;
18+
export type Error = AxiosError | HttpException;
1819

1920
export type Authentication = UtilityTypes.XOR3<
2021
{

0 commit comments

Comments
 (0)