Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@aircall/http

Package Overview
Dependencies
Maintainers
8
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@aircall/http - npm Package Compare versions

Comparing version 0.3.0 to 0.4.0

11

CHANGELOG.md

@@ -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 @@

23

dist/HttpService.d.ts

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc