@aircall/http
Advanced tools
Comparing version 0.3.0 to 0.4.0
@@ -6,2 +6,13 @@ # Change Log | ||
# [0.4.0](http://bitbucket.org/aircall/front-end-modules/compare/@aircall/http@0.3.0...@aircall/http@0.4.0) (2021-04-06) | ||
### Features | ||
* **http:** move error handler to config ([0004f5d](http://bitbucket.org/aircall/front-end-modules/commits/0004f5d2f585888c068a96a4ab612638acc49868)) | ||
# [0.3.0](http://bitbucket.org/aircall/front-end-modules/compare/@aircall/http@0.2.2...@aircall/http@0.3.0) (2021-03-19) | ||
@@ -8,0 +19,0 @@ |
@@ -1,2 +0,2 @@ | ||
import { AxiosError, AxiosPromise, AxiosRequestConfig } from 'axios'; | ||
import { AxiosError, AxiosRequestConfig } from 'axios'; | ||
import { HttpServiceOptions, RequestLogPayload, HTTP_LOG_TYPE } from './typing/HttpService'; | ||
@@ -8,3 +8,2 @@ declare class HttpService { | ||
private logger; | ||
private axiosExternalInstance; | ||
constructor(options: HttpServiceOptions); | ||
@@ -34,18 +33,8 @@ setToken(token: string): void; | ||
private handleResponse; | ||
handleError: (error: AxiosError<any>) => Error; | ||
get(path: string, config?: AxiosRequestConfig): AxiosPromise<object>; | ||
post(path: string, payload?: {}, config?: AxiosRequestConfig): AxiosPromise<object>; | ||
patch(path: string, payload?: {}, config?: AxiosRequestConfig): Promise<object>; | ||
put(path: string, payload?: {}, config?: AxiosRequestConfig): Promise<object>; | ||
delete(path: string, config?: AxiosRequestConfig): Promise<object>; | ||
/** | ||
* Return the first error message on the first field from the Api response | ||
* {foo:bar} will return null | ||
* {error:{fieldName1:['a']}} will return 'a' | ||
* {error:{fieldName1:['a', 'b']}} will return 'a' | ||
* {error:{fieldName1:['a', 'b'],fieldName2:['c', 'd']}} will return 'a' | ||
* @param body | ||
*/ | ||
private getFirstErrorMessageOnFirstField; | ||
get<T = any>(path: string, config?: AxiosRequestConfig): Promise<T>; | ||
post<T = any>(path: string, payload?: {}, config?: AxiosRequestConfig): Promise<T>; | ||
patch<T = any>(path: string, payload?: {}, config?: AxiosRequestConfig): Promise<T>; | ||
put<T = any>(path: string, payload?: {}, config?: AxiosRequestConfig): Promise<T>; | ||
delete<T = any>(path: string, config?: AxiosRequestConfig): Promise<T>; | ||
} | ||
export default HttpService; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const axios_1 = require("axios"); | ||
const constants_1 = require("./constants"); | ||
// Fallback for unsupported error response | ||
const defaultInvalidApiResponseErr = { | ||
code: constants_1.ErrorCode.INVALID_API_RESPONSE_CODE, | ||
message: constants_1.INVALID_API_RESPONSE_MESSAGE | ||
}; | ||
class HttpService { | ||
@@ -64,27 +58,3 @@ constructor(options) { | ||
}; | ||
this.handleError = (error) => { | ||
let err; | ||
if (error.response) { | ||
// The request was made and the server responded with a status code | ||
// that falls out of the range of 2xx | ||
const errorMessage = this.getFirstErrorMessageOnFirstField(error.response); | ||
err = { | ||
code: error.response.status || constants_1.ErrorCode.INVALID_API_RESPONSE_CODE, | ||
message: errorMessage || constants_1.INVALID_API_RESPONSE_MESSAGE | ||
}; | ||
} | ||
else if (error.request) { | ||
// The request was made but no response was received | ||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of | ||
// http.ClientRequest in node.js | ||
err = defaultInvalidApiResponseErr; | ||
} | ||
else { | ||
// Something happened in setting up the request that triggered an Error | ||
err = defaultInvalidApiResponseErr; | ||
} | ||
// TODO log error stacktrace (n-1 or n-2 method) | ||
throw err; | ||
}; | ||
const { apiBaseUrl, logger, headers } = this.options; | ||
const { apiBaseUrl, logger, headers, handleError } = this.options; | ||
if (!apiBaseUrl) { | ||
@@ -107,6 +77,5 @@ throw new Error('Missing API URL!'); | ||
this.axiosInstance.interceptors.response.use(this.logResponseInterceptor, this.logErrorInterceptor); | ||
// Separate instance for making external (non Aircall API) requests | ||
this.axiosExternalInstance = axios_1.default.create(); | ||
this.axiosExternalInstance.interceptors.request.use(this.logRequestInterceptor); | ||
this.axiosExternalInstance.interceptors.response.use(this.logResponseInterceptor, this.logErrorInterceptor); | ||
if (handleError) { | ||
this.axiosInstance.interceptors.response.use(value => value, handleError); | ||
} | ||
} | ||
@@ -126,50 +95,16 @@ setToken(token) { | ||
get(path, config) { | ||
return this.axiosInstance.get(path, config).then(this.handleResponse).catch(this.handleError); | ||
return this.axiosInstance.get(path, config).then(this.handleResponse); | ||
} | ||
post(path, payload = {}, config) { | ||
return this.axiosInstance | ||
.post(path, payload, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
return this.axiosInstance.post(path, payload, config).then(this.handleResponse); | ||
} | ||
patch(path, payload = {}, config) { | ||
return this.axiosInstance | ||
.patch(path, payload, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
return this.axiosInstance.patch(path, payload, config).then(this.handleResponse); | ||
} | ||
put(path, payload = {}, config) { | ||
return this.axiosInstance | ||
.put(path, payload, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
return this.axiosInstance.put(path, payload, config).then(this.handleResponse); | ||
} | ||
delete(path, config) { | ||
return this.axiosInstance | ||
.delete(path, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
return this.axiosInstance.delete(path, config).then(this.handleResponse); | ||
} | ||
/** | ||
* Return the first error message on the first field from the Api response | ||
* {foo:bar} will return null | ||
* {error:{fieldName1:['a']}} will return 'a' | ||
* {error:{fieldName1:['a', 'b']}} will return 'a' | ||
* {error:{fieldName1:['a', 'b'],fieldName2:['c', 'd']}} will return 'a' | ||
* @param body | ||
*/ | ||
getFirstErrorMessageOnFirstField(errorResponse) { | ||
var _a, _b; | ||
if (!errorResponse || !errorResponse.data) { | ||
// No "body.error" | ||
return null; | ||
} | ||
const fields = Object.keys(errorResponse.data); | ||
if (!fields || !fields.length || !fields[0] || !fields[0].length) { | ||
// Empty "body.error" | ||
return null; | ||
} | ||
// Get the first error on the first field | ||
return ((_b = (_a = errorResponse.data) === null || _a === void 0 ? void 0 : _a[fields[0]]) === null || _b === void 0 ? void 0 : _b[0]) || null; | ||
} | ||
} | ||
@@ -176,0 +111,0 @@ HttpService.getRequestLogPayload = (config) => { |
import { AxiosRequestConfig, AxiosError } from 'axios'; | ||
import { Logger } from '@aircall/logger'; | ||
export interface ApiError { | ||
data: Record<string, string[]>; | ||
} | ||
export interface ApiResponseError { | ||
code?: number; | ||
message: string; | ||
} | ||
export interface RequestLogPayload { | ||
@@ -26,3 +19,5 @@ request_base_url: AxiosRequestConfig['baseURL']; | ||
logErrorInterceptor?: (error: AxiosError) => Promise<AxiosError> | void; | ||
handleError?: (error: AxiosError) => Promise<AxiosError>; | ||
} | ||
export declare type HttpError<T> = AxiosError<T>; | ||
export {}; |
{ | ||
"name": "@aircall/http", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"main": "dist/index.js", | ||
@@ -14,3 +14,3 @@ "types": "dist/index.d.ts", | ||
}, | ||
"gitHead": "8ab9258a5f144ed095bc32435f51aa619f47c4c9", | ||
"gitHead": "4b7fc5f9dffe6ccabfffdf820128ead7c6dbcc6f", | ||
"dependencies": { | ||
@@ -17,0 +17,0 @@ "@aircall/logger": "^2.5.2", |
import axios, { AxiosError } from 'axios'; | ||
import MockAdapter from 'axios-mock-adapter'; | ||
import { Logger } from '@aircall/logger'; | ||
import { ErrorCode, INVALID_API_RESPONSE_MESSAGE } from './constants'; | ||
import { HttpServiceOptions } from './typing/HttpService'; | ||
import HttpService from './'; | ||
@@ -24,3 +25,3 @@ | ||
describe('constructor', () => { | ||
it('should throw an error if the class is instantiated without URL', async () => { | ||
it('should throw an error if the class is instantiated without URL', () => { | ||
try { | ||
@@ -33,3 +34,3 @@ httpService = new HttpService({ apiBaseUrl: undefined, logger }); | ||
it('should throw an error if the class is instantiated without logger', async () => { | ||
it('should throw an error if the class is instantiated without logger', () => { | ||
try { | ||
@@ -41,2 +42,41 @@ httpService = new HttpService({ apiBaseUrl: BASE_URL, logger: undefined }); | ||
}); | ||
it('should register a global "handleError" interceptor if provided in config', async () => { | ||
const handleError: jest.Mocked< | ||
HttpServiceOptions['handleError'] | ||
> = jest.fn().mockImplementation(error => Promise.reject(error)); | ||
const data = { message: 'error message' }; | ||
const status = 400; | ||
httpService = new HttpService({ apiBaseUrl: BASE_URL, logger, handleError }); | ||
mock.onGet(EXAMPLE_PATH).reply(status, data); | ||
try { | ||
await httpService.get(EXAMPLE_PATH); | ||
} catch (error) { | ||
expect(error.isAxiosError).toBe(true); | ||
expect(error.response.data).toEqual(data); | ||
} | ||
expect(handleError).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
response: expect.objectContaining({ status, data }) | ||
}) | ||
); | ||
}); | ||
it('should correctly handle successfull requests when the global "handleError" interceptor is registered', async () => { | ||
const handleError: jest.Mocked< | ||
HttpServiceOptions['handleError'] | ||
> = jest.fn().mockImplementation(error => Promise.reject(error)); | ||
const data = { message: 'message' }; | ||
const status = 200; | ||
httpService = new HttpService({ apiBaseUrl: BASE_URL, logger, handleError }); | ||
mock.onGet(EXAMPLE_PATH).reply(status, data); | ||
const response = await httpService.get(EXAMPLE_PATH); | ||
expect(response).toEqual(data); | ||
}); | ||
}); | ||
@@ -163,94 +203,2 @@ | ||
describe('getFirstErrorMessageOnFirstField', () => { | ||
it('should return null when body is null', () => { | ||
// @ts-ignore | ||
expect(httpService.getFirstErrorMessageOnFirstField(null)).toEqual(null); | ||
}); | ||
it('should return null when body.error is null', () => { | ||
// @ts-ignore | ||
expect(httpService.getFirstErrorMessageOnFirstField({ data: null })).toEqual(null); | ||
}); | ||
it('should return null when body.error is empty', () => { | ||
// @ts-ignore | ||
expect(httpService.getFirstErrorMessageOnFirstField({ data: {} })).toEqual(null); | ||
}); | ||
it('should return null when body.error.fieldName is null', () => { | ||
// @ts-ignore | ||
expect(httpService.getFirstErrorMessageOnFirstField({ data: { fieldName: null } })).toEqual( | ||
null | ||
); | ||
}); | ||
it('should return null when body.error.fieldName is an empty array', () => { | ||
// @ts-ignore | ||
expect(httpService.getFirstErrorMessageOnFirstField({ data: { fieldName: [] } })).toEqual( | ||
null | ||
); | ||
}); | ||
it('should return first error message when body.error.fieldName[0] exists', () => { | ||
const firstErrorMessage = 'First error message'; | ||
const secondErrorMessage = 'Second error message'; | ||
// @ts-ignore | ||
const result = httpService.getFirstErrorMessageOnFirstField({ | ||
data: { | ||
fieldName: [firstErrorMessage, secondErrorMessage] | ||
} | ||
}); | ||
expect(result).toEqual(firstErrorMessage); | ||
}); | ||
}); | ||
describe('handleError', () => { | ||
it('should throw the default error if an unknown one is given', () => { | ||
try { | ||
httpService.handleError({} as AxiosError); | ||
} catch (e) { | ||
expect(e.message).toBe(INVALID_API_RESPONSE_MESSAGE); | ||
expect(e.code).toBe(ErrorCode.INVALID_API_RESPONSE_CODE); | ||
} | ||
}); | ||
it('should throw the default error if the given error has no response but a request attribute', () => { | ||
try { | ||
httpService.handleError({ request: {} } as AxiosError); | ||
} catch (e) { | ||
expect(e.message).toBe(INVALID_API_RESPONSE_MESSAGE); | ||
expect(e.code).toBe(ErrorCode.INVALID_API_RESPONSE_CODE); | ||
} | ||
}); | ||
it('should throw an error with defaut message and code if the given error has an empty reponse attribute', () => { | ||
try { | ||
httpService.handleError({ response: {} } as AxiosError); | ||
} catch (e) { | ||
expect(e.message).toBe(INVALID_API_RESPONSE_MESSAGE); | ||
expect(e.code).toBe(ErrorCode.INVALID_API_RESPONSE_CODE); | ||
} | ||
}); | ||
it('should throw an error with message and code if the given error is complete', () => { | ||
const customErrorMessage = 'Custom error message'; | ||
// @ts-ignore | ||
spyOn(httpService, 'getFirstErrorMessageOnFirstField').and.returnValue(customErrorMessage); | ||
try { | ||
httpService.handleError({ | ||
response: { | ||
status: ErrorCode.UNAUTHORIZED_API_RESPONSE_CODE, | ||
data: 'Error from the server' | ||
} | ||
} as AxiosError); | ||
} catch (e) { | ||
expect(e.message).toBe(customErrorMessage); | ||
expect(e.code).toBe(ErrorCode.UNAUTHORIZED_API_RESPONSE_CODE); | ||
} | ||
}); | ||
}); | ||
describe('getUrlFromConfig', () => { | ||
@@ -257,0 +205,0 @@ it('should return the url if it is undefined', () => { |
@@ -1,26 +0,6 @@ | ||
import axios, { | ||
AxiosError, | ||
AxiosInstance, | ||
AxiosPromise, | ||
AxiosRequestConfig, | ||
AxiosResponse | ||
} from 'axios'; | ||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; | ||
import { Logger } from '@aircall/logger'; | ||
import { ErrorCode, INVALID_API_RESPONSE_MESSAGE } from './constants'; | ||
import { | ||
ApiResponseError, | ||
HttpServiceOptions, | ||
RequestLogPayload, | ||
SearchParams, | ||
ApiError, | ||
HTTP_LOG_TYPE | ||
} from './typing/HttpService'; | ||
import { HttpServiceOptions, RequestLogPayload, HTTP_LOG_TYPE } from './typing/HttpService'; | ||
// Fallback for unsupported error response | ||
const defaultInvalidApiResponseErr: ApiResponseError = { | ||
code: ErrorCode.INVALID_API_RESPONSE_CODE, | ||
message: INVALID_API_RESPONSE_MESSAGE | ||
}; | ||
class HttpService { | ||
@@ -30,6 +10,5 @@ private token?: string; | ||
private logger: Logger; | ||
private axiosExternalInstance: AxiosInstance; | ||
public constructor(private options: HttpServiceOptions) { | ||
const { apiBaseUrl, logger, headers } = this.options; | ||
const { apiBaseUrl, logger, headers, handleError } = this.options; | ||
@@ -67,9 +46,5 @@ if (!apiBaseUrl) { | ||
// Separate instance for making external (non Aircall API) requests | ||
this.axiosExternalInstance = axios.create(); | ||
this.axiosExternalInstance.interceptors.request.use(this.logRequestInterceptor); | ||
this.axiosExternalInstance.interceptors.response.use( | ||
this.logResponseInterceptor, | ||
this.logErrorInterceptor | ||
); | ||
if (handleError) { | ||
this.axiosInstance.interceptors.response.use(value => value, handleError); | ||
} | ||
} | ||
@@ -81,7 +56,7 @@ | ||
public removeToken() { | ||
public removeToken(): void { | ||
this.token = undefined; | ||
} | ||
public getToken() { | ||
public getToken(): string | undefined { | ||
return this.token; | ||
@@ -176,86 +151,27 @@ } | ||
private handleResponse = (response: AxiosResponse): AxiosResponse['data'] => { | ||
private handleResponse = <T = any>(response: AxiosResponse<T>): AxiosResponse<T>['data'] => { | ||
return response.data; | ||
}; | ||
public handleError = (error: AxiosError): Error => { | ||
let err: ApiResponseError; | ||
if (error.response) { | ||
// The request was made and the server responded with a status code | ||
// that falls out of the range of 2xx | ||
const errorMessage: string | null = this.getFirstErrorMessageOnFirstField(error.response); | ||
err = { | ||
code: error.response.status || ErrorCode.INVALID_API_RESPONSE_CODE, | ||
message: errorMessage || INVALID_API_RESPONSE_MESSAGE | ||
}; | ||
} else if (error.request) { | ||
// The request was made but no response was received | ||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of | ||
// http.ClientRequest in node.js | ||
err = defaultInvalidApiResponseErr; | ||
} else { | ||
// Something happened in setting up the request that triggered an Error | ||
err = defaultInvalidApiResponseErr; | ||
} | ||
// TODO log error stacktrace (n-1 or n-2 method) | ||
throw err; | ||
}; | ||
public get(path: string, config?: AxiosRequestConfig): AxiosPromise<object> { | ||
return this.axiosInstance.get(path, config).then(this.handleResponse).catch(this.handleError); | ||
public get<T = any>(path: string, config?: AxiosRequestConfig): Promise<T> { | ||
return this.axiosInstance.get<T>(path, config).then(this.handleResponse); | ||
} | ||
public post(path: string, payload: {} = {}, config?: AxiosRequestConfig): AxiosPromise<object> { | ||
return this.axiosInstance | ||
.post(path, payload, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
public post<T = any>(path: string, payload: {} = {}, config?: AxiosRequestConfig): Promise<T> { | ||
return this.axiosInstance.post<T>(path, payload, config).then(this.handleResponse); | ||
} | ||
public patch(path: string, payload: {} = {}, config?: AxiosRequestConfig): Promise<object> { | ||
return this.axiosInstance | ||
.patch(path, payload, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
public patch<T = any>(path: string, payload: {} = {}, config?: AxiosRequestConfig): Promise<T> { | ||
return this.axiosInstance.patch<T>(path, payload, config).then(this.handleResponse); | ||
} | ||
public put(path: string, payload: {} = {}, config?: AxiosRequestConfig): Promise<object> { | ||
return this.axiosInstance | ||
.put(path, payload, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
public put<T = any>(path: string, payload: {} = {}, config?: AxiosRequestConfig): Promise<T> { | ||
return this.axiosInstance.put<T>(path, payload, config).then(this.handleResponse); | ||
} | ||
public delete(path: string, config?: AxiosRequestConfig): Promise<object> { | ||
return this.axiosInstance | ||
.delete(path, config) | ||
.then(this.handleResponse) | ||
.catch(this.handleError); | ||
public delete<T = any>(path: string, config?: AxiosRequestConfig): Promise<T> { | ||
return this.axiosInstance.delete<T>(path, config).then(this.handleResponse); | ||
} | ||
/** | ||
* Return the first error message on the first field from the Api response | ||
* {foo:bar} will return null | ||
* {error:{fieldName1:['a']}} will return 'a' | ||
* {error:{fieldName1:['a', 'b']}} will return 'a' | ||
* {error:{fieldName1:['a', 'b'],fieldName2:['c', 'd']}} will return 'a' | ||
* @param body | ||
*/ | ||
private getFirstErrorMessageOnFirstField(errorResponse: ApiError): string | null { | ||
if (!errorResponse || !errorResponse.data) { | ||
// No "body.error" | ||
return null; | ||
} | ||
const fields: string[] = Object.keys(errorResponse.data); | ||
if (!fields || !fields.length || !fields[0] || !fields[0].length) { | ||
// Empty "body.error" | ||
return null; | ||
} | ||
// Get the first error on the first field | ||
return errorResponse.data?.[fields[0]]?.[0] || null; | ||
} | ||
} | ||
export default HttpService; |
import { AxiosRequestConfig, AxiosError } from 'axios'; | ||
import { Logger } from '@aircall/logger'; | ||
// Rails error response | ||
export interface ApiError { | ||
data: Record<string, string[]>; | ||
} | ||
export interface ApiResponseError { | ||
code?: number; | ||
message: string; | ||
} | ||
export interface RequestLogPayload { | ||
@@ -33,2 +23,5 @@ request_base_url: AxiosRequestConfig['baseURL']; | ||
logErrorInterceptor?: (error: AxiosError) => Promise<AxiosError> | void; | ||
handleError?: (error: AxiosError) => Promise<AxiosError>; | ||
} | ||
export type HttpError<T> = AxiosError<T>; |
Sorry, the diff of this file is not supported yet
32109
607