Skip to content

Commit

Permalink
feat: Adds new HttpException class which will be return instead of th… (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
bk201- authored May 16, 2024
1 parent 4a17531 commit 1500de9
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 22 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Usability, consistency, and performance are key focuses of jira.js, and it also
- [Basic](#basic-authentication)
- [OAuth 2.0](#oauth-20)
- [Personal access token](#personal-access-token)
- [Error handling](#error-handling)
- [Example and using algorithm](#example-and-using-algorithm)
- [Decreasing Webpack bundle size](#decreasing-webpack-bundle-size)
- [Take a look at our other products](#take-a-look-at-our-other-products)
Expand Down Expand Up @@ -123,6 +124,34 @@ const client = new Version3Client({
});
```

#### Error handling
Starting from version 4.0.0, the library has a new error handling system.
Now, all errors are instances of
- the `HttpException` class in case the Axios has response from the server;
- the `AxiosError` class in case something went wrong before sending the request.

The `HttpException` class tries to parse different sorts of responses from the server to provide a unified error class.

If the original error is required, you can get it from the `cause` property of the `HttpException` class.

```typescript
try {
const users = await this.client.userSearch.findUsers({ query: email });
// ...
} catch (error: uknown) {
if (error instanceof HttpException) {
console.log(error.message);
console.log(error.cause); // original error (AxiosError | Error)
console.log(error.cause.response?.headers); // headers from the server
} else if (error instanceof AxiosError) {
console.log(error.message);
console.log(error.code); // error code, for instance AxiosError.ETIMEDOUT
} else {
console.log(error);
}
}
````

#### Example and using algorithm

1. Example
Expand Down
4 changes: 2 additions & 2 deletions src/callback.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { AxiosError } from 'axios';
import { Config } from './config';

export type Callback<T> = (err: AxiosError | null, data?: T) => void;
export type Callback<T> = (err: Config.Error | null, data?: T) => void;
52 changes: 33 additions & 19 deletions src/clients/baseClient.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import type { Callback } from '../callback';
import type { Client } from './client';
import type { Config } from '../config';
import { getAuthenticationToken } from '../services/authenticationService';
import type { RequestConfig } from '../requestConfig';
import { HttpException, isObject } from './httpException';

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

return this.handleSuccessResponse(response.data, callback);
} catch (e: any) {
} catch (e: unknown) {
return this.handleFailedResponse(e, callback);
}
}
Expand Down Expand Up @@ -119,11 +120,11 @@ export class BaseClient implements Client {
return responseHandler(response);
}

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

const callbackErrorHandler = callback && ((error: Config.Error) => callback(error));
const defaultErrorHandler = (error: Error) => {
const defaultErrorHandler = (error: Config.Error) => {
throw error;
};

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

private buildErrorHandlingResponse(error: AxiosError<any>) {
const headers = error.response?.headers ?? {};
const responseData = error.response?.data ?? {};
const data = typeof responseData === 'object' ? responseData : { data: responseData };
private buildErrorHandlingResponse(e: unknown): Config.Error {
if (axios.isAxiosError(e) && e.response) {
return new HttpException(
{
code: e.code,
message: e.message,
data: e.response.data,
status: e.response?.status,
statusText: e.response?.statusText,
},
e.response.status,
{ cause: e },
);
}

return {
code: error.code,
headers: this.removeUndefinedProperties({
[RETRY_AFTER]: headers[RETRY_AFTER],
[RATE_LIMIT_RESET]: headers[RATE_LIMIT_RESET],
}),
status: error.response?.status,
statusText: error.response?.statusText,
...data,
};
if (axios.isAxiosError(e)) {
return e;
}

if (isObject(e) && isObject((e as Record<string, any>).response)) {
return new HttpException((e as Record<string, any>).response);
}

if (e instanceof Error) {
return new HttpException(e);
}

return new HttpException('Unknown error occurred.', 500, { cause: e });
}
}
137 changes: 137 additions & 0 deletions src/clients/httpException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
export const isUndefined = (obj: any): obj is undefined => typeof obj === 'undefined';

export const isNil = (val: any): val is null | undefined => isUndefined(val) || val === null;

export const isObject = (fn: any): fn is object => !isNil(fn) && typeof fn === 'object';

export const isString = (val: any): val is string => typeof val === 'string';

export const isNumber = (val: any): val is number => typeof val === 'number';

export interface HttpExceptionOptions {
/** Original cause of the error */
cause?: unknown;
description?: string;
}

export const DEFAULT_EXCEPTION_STATUS = 500;
export const DEFAULT_EXCEPTION_MESSAGE = 'Something went wrong';
export const DEFAULT_EXCEPTION_CODE = 'INTERNAL_SERVER_ERROR';
export const DEFAULT_EXCEPTION_STATUS_TEXT = 'Internal server error';

/** Defines the base HTTP exception, which is handled by the default Exceptions Handler. */
export class HttpException extends Error {
/**
* Instantiate a plain HTTP Exception.
*
* @example
* throw new HttpException('message', HttpStatus.BAD_REQUEST);
* throw new HttpException('custom message', HttpStatus.BAD_REQUEST, {
* cause: new Error('Cause Error'),
* });
*
* @param response String, object describing the error condition or the error cause.
* @param status HTTP response status code.
* @param options An object used to add an error cause. Configures error chaining support
* @usageNotes
* The constructor arguments define the response and the HTTP response status code.
* - The `response` argument (required) defines the JSON response body. alternatively, it can also be
* an error object that is used to define an error [cause](https://nodejs.org/en/blog/release/v16.9.0/#error-cause).
* - The `status` argument (optional) defines the HTTP Status Code.
* - The `options` argument (optional) defines additional error options. Currently, it supports the `cause` attribute,
* and can be used as an alternative way to specify the error cause: `const error = new HttpException('description', 400, { cause: new Error() });`
*
* By default, the JSON response body contains two properties:
* - `statusCode`: the Http Status Code.
* - `message`: a short description of the HTTP error by default; override this
* by supplying a string in the `response` parameter.
*
* The `status` argument is required, and should be a valid HTTP status code.
* Best practice is to use the `HttpStatus` enum imported from `nestjs/common`.
* @see https://nodejs.org/en/blog/release/v16.9.0/#error-cause
* @see https://github.com/microsoft/TypeScript/issues/45167
*/
constructor(
public readonly response: string | Record<string, any>,
status?: number,
options?: HttpExceptionOptions,
) {
super();

this.name = this.initName();
this.cause = this.initCause(response, options);
this.code = this.initCode(response);
this.message = this.initMessage(response);
this.status = this.initStatus(response, status);
this.statusText = this.initStatusText(response, this.status);
}

public readonly cause?: unknown;
public readonly code?: string;
public readonly status: number;
public readonly statusText?: string;

protected initMessage(response: string | Record<string, any>) {
if (isString(response)) {
return response;
}

if (isObject(response) && isString((response as Record<string, any>).message)) {
return (response as Record<string, any>).message;
}

if (this.constructor) {
return this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g)?.join(' ') ?? 'Error';
}

return DEFAULT_EXCEPTION_MESSAGE;
}

protected initCause(response: string | Record<string, any>, options?: HttpExceptionOptions): unknown {
if (options?.cause) {
return options.cause;
}

if (isObject(response) && isObject((response as Record<string, any>).cause)) {
return (response as Record<string, any>).cause;
}

return undefined;
}

protected initCode(response: string | Record<string, any>): string {
if (isObject(response) && isString((response as Record<string, any>).code)) {
return (response as Record<string, any>).code;
}

return DEFAULT_EXCEPTION_CODE;
}

protected initName(): string {
return this.constructor.name;
}

protected initStatus(response: string | Record<string, any>, status?: number): number {
if (status) {
return status;
}

if (isObject(response) && isNumber((response as Record<string, any>).status)) {
return (response as Record<string, any>).status;
}

if (isObject(response) && isNumber((response as Record<string, any>).statusCode)) {
return (response as Record<string, any>).statusCode;
}

return DEFAULT_EXCEPTION_STATUS;
}

protected initStatusText(response: string | Record<string, any>, status?: number): string | undefined {
if (isObject(response) && isString((response as Record<string, any>).statusText)) {
return (response as Record<string, any>).statusText;
}

return status ? undefined : DEFAULT_EXCEPTION_STATUS_TEXT;
}
}
1 change: 1 addition & 0 deletions src/clients/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './baseClient';
export * from './client';
export * from './httpException';

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

Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AxiosError } from 'axios';
import { RequestConfig } from './requestConfig';
import { UtilityTypes } from './utilityTypes';
import { HttpException } from './clients';

export interface Config {
host: string;
Expand All @@ -14,7 +15,7 @@ export interface Config {

export namespace Config {
export type BaseRequestConfig = RequestConfig;
export type Error = AxiosError;
export type Error = AxiosError | HttpException;

export type Authentication = UtilityTypes.XOR3<
{
Expand Down

0 comments on commit 1500de9

Please sign in to comment.