import qs from 'qs' ; import { setupCache } from 'axios-cache-adapter' ; import axios , { AxiosInstance , AxiosRequestConfig , AxiosResponse } from 'axios' ; import { env } from '../../env' ; import { logger } from '../../logger' ; import { TwilioApiError } from '../../errors' ; type RequestInterceptor = ( req : AxiosRequestConfig ) => Promise < AxiosRequestConfig > ; interface Concurrency { pending : number ; max : number ; } interface RequestOption { cacheable ? : boolean ; cacheAge ? : number ; } export interface AuthConfig { username : string ; password : string ; } export interface OptionalHttpClientConfig { caller ? : string ; setUserAgent ? : boolean ; packages ? : { [ key ] : string ; } ; auth ? : AuthConfig ; headers ? : { [ key ] : string ; } ; requestInterceptors ? : [ RequestInterceptor ] ; maxConcurrentRequests ? : number ; } export interface HttpClientConfig extends OptionalHttpClientConfig { baseURL : string ; } export default class Http { static DEFAULT_CONCURRENT_REQUESTS = 5 ; static CONCURRENCY_REQUEST_LOOP_MS_DELAY = 100 ; static ContentType = 'application/x-www-form-urlencoded' ; static UserAgent = 'User-Agent' ; protected readonly client : AxiosInstance ; protected readonly cacheAge : number ; protected readonly concurrency : Concurrency ; constructor ( config ) { this . cacheAge = 15 * 60 * 1000 ; const cache = setupCache ( { maxAge : 0 } ) ; const axiosConfig = { baseURL : config . baseURL , headers : { 'Content-Type' : Http . ContentType , ... config . headers , } , adapter : cache . adapter , } ; if ( config . auth ) { axiosConfig . auth = { username : config . auth . username , password : config . auth . password , } ; } if ( config . setUserAgent ) { if ( ! axiosConfig . headers ) { axiosConfig . headers = { } ; } axiosConfig . headers [ Http . UserAgent ] = Http . getUserAgent ( config ) ; } this . client = axios . create ( axiosConfig ) ; this . concurrency = { pending : 0 , max : config . maxConcurrentRequests || Http . DEFAULT_CONCURRENT_REQUESTS , } ; this . client . interceptors . request . use ( this . useRequestInterceptors ( [ this . concurrencyRequestTransform , Http . transformRequestFormData ] . concat ( config . requestInterceptors || [ ] ) ) ) ; this . client . interceptors . response . use ( this . transformResponse , this . transformResponseError ) ; } param config private static getUserAgent ( config ) { const packages = config . packages || { } ; const userAgent = [ ] ; if ( env . isNode ( ) ) { userAgent . push ( `Node.js/${ process . version . slice ( 1 ) }` , `(${ process . platform }; ${ process . arch })` ) ; const shell = process . env . SHELL ?. split ( '/' ) . pop ( ) || 'unknown' ; userAgent . push ( `shell/${ shell }` ) ; if ( process . versions . yarn ) { userAgent . push ( `yarn/${ process . versions . yarn }` ) ; } if ( process . versions . npm ) { userAgent . push ( `npm/${ process . versions . npm }` ) ; } } else { userAgent . push ( window . navigator . userAgent ) ; } if ( config . caller ) { userAgent . push ( `caller/${ config . caller }` ) ; } Object . entries ( packages ) . forEach ( ( [ key , value ] ) => userAgent . push ( `${ key }/${ value }` ) ) ; userAgent . push ( `is_ci/${ env . isCI ( ) }` ) ; return userAgent . join ( ' ' ) ; } param obj private static prettyPrint ( obj ) { return JSON . stringify ( obj , null , 2 ) ; } param err private static isTwilioError ( err ) { return Boolean ( err && err . isAxiosError && err . response && err . response . data && err . response . data . more_info ) ; } param req private static async transformRequestFormData ( req ) { const method = req . method ? req . method : 'GET' ; logger . debug ( `Making a ${ method . toUpperCase ( ) } to ${ req . baseURL }/${ req . url }` ) ; if ( method . toLocaleLowerCase ( ) === 'post' && req . headers ?. [ 'Content-Type' ] === Http . ContentType && typeof req . data === 'object' ) { const data = Object . keys ( req . data ) . map ( ( key ) => { if ( ! Array . isArray ( req . data [ key ] ) ) { return { [ key ] : req . data [ key ] } ; } const value = req . data [ key ] . map ( ( v ) => { if ( typeof v !== 'object' ) { return v ; } return JSON . stringify ( v ) ; } ) ; return { [ key ] : value } ; } ) ; req . data = qs . stringify ( Object . assign ( { } , ... data ) , { encode : false , arrayFormat : 'repeat' } ) ; } return Promise . resolve ( req ) ; } param uri param option public async get < R > ( uri , option ? ) { return this . client . get ( uri , this . getRequestOption ( option ) ) ; } param uri param data public async post < R > ( uri , data ) { return this . client . post ( uri , data ) ; } param uri public async delete ( uri ) { return this . client . delete ( uri ) ; } param interceptors private private useRequestInterceptors ( interceptors ) { return async ( req ) : Promise < AxiosRequestConfig > => { return interceptors . reduce ( async ( chain , current ) => chain . then ( current ) , Promise . resolve ( req ) ) ; } ; } param req private private concurrencyRequestTransform = async ( req ) : Promise < AxiosRequestConfig > => { return new Promise ( ( resolve ) => { const interval = setInterval ( ( ) => { const { pending , max } = this . concurrency ; logger . debug ( '%s pending HTTP requests - max concurrent requests allowed is %s' , pending , max ) ; if ( pending < max ) { this . incrementConcurrentRequests ( ) ; clearInterval ( interval ) ; resolve ( req ) ; return ; } logger . debug ( 'Reached %s concurrent requests, waiting for requests to resolve' , max ) ; } , Http . CONCURRENCY_REQUEST_LOOP_MS_DELAY ) ; } ) ; } ; param resp private transformResponse = ( resp ) => { this . decrementConcurrentRequests ( ) ; const { data } = resp ; const servedFromCache = resp . request . fromCache === true ? '(served from cache) ' : '' ; const pretty = Http . prettyPrint ( data ) ; const url = `${ resp . config . baseURL }/${ resp . config . url }` ; const method = resp . request . method || '' ; logger . debug ( `${ method } request to ${ url } ${ servedFromCache }responded with statusCode ${ resp . status } and data\n${ pretty }\n` ) ; return data ; } ; param err private transformResponseError = async ( err ) => { this . decrementConcurrentRequests ( ) ; if ( Http . isTwilioError ( err ) ) { const { data } = err . response ; logger . debug ( `Request errored with data\n${ Http . prettyPrint ( data ) }` ) ; return Promise . reject ( new TwilioApiError ( data . code , data . message , data . status , data . more_info ) ) ; } logger . debug ( `Request errored with message ${ err . message }` ) ; return Promise . reject ( err ) ; } ; /**
     * Increments concurrent request counter
     */ private incrementConcurrentRequests = ( ) => { this . concurrency . pending += 1 ; } ; /**
     * Decrements concurrent request counter
     */ private decrementConcurrentRequests = ( ) => { this . concurrency . pending = Math . max ( 0 , this . concurrency . pending - 1 ) ; } ; /**
     * Returns a  AxiosRequestConfig configuration
     *  param option private getRequestOption ( option ? ) { const opt = { } ; if ( ! option ) { return opt ; } if ( option . cacheable ) { opt . cache = { maxAge : option . cacheAge || this . cacheAge , } ; } return opt ; } }