/* * This code was auto generated by AfterShip SDK Generator. * Do not edit the class manually. */ import axios, { AxiosRequestConfig } from "axios"; import { AuthType, Authentication } from "./authentication"; import { AftershipError } from "../error"; import { AfterShipErrorCodes } from "../error/code"; import { AfterShipMetaCodeMap } from "../error/meta_code"; import { Proxy } from "../utils/parse_proxy"; import querystring from "querystring"; export const DEFAULT_DOMAIN = "https://api.aftership.com"; export const DEFAULT_TIMEOUT = 10000; export const DEFAULT_MAX_RETRY = 3; export const MAX_MAX_RETRY = 10; export const MIN_MAX_RETRY = 0; export const DEFAULT_USER_AGENT = "tracking-sdk-nodejs/17.0.0 (https://www.aftership.com) axios/1.7.2"; type ResponseData = { meta: { code: number; message: number; type: string; }; data: any; }; export interface RequestConfig { url: string; method: string; body?: any; query?: any; headers?: { [key: string]: any }; } export interface RequestOptions { auth_type: string; api_key?: string; api_secret?: string; domain: string; max_retry: number; timeout: number; user_agent?: string; proxy?: Proxy; } export class Request { private readonly options: RequestOptions; private readonly DELAY_BASE = 3000; constructor(options: RequestOptions) { this.options = options; } private getHeaders(config: RequestConfig): { [key: string]: any } { const content_type = "application/json"; const headers: { [key: string]: any } = { "as-api-key": this.options.api_key, "Content-Type": content_type, "User-Agent": this.options.user_agent || DEFAULT_USER_AGENT, "aftership-client": DEFAULT_USER_AGENT, ...config.headers, }; if ( this.options.auth_type === AuthType.AES || this.options.auth_type === AuthType.RSA ) { const header_keys = this.options.auth_type === AuthType.AES ? "as-signature-hmac-sha256" : "as-signature-rsa-sha256"; const date_now = new Date().toUTCString(); headers[header_keys] = Authentication.sign({ method: config.method, url: config.url, body: JSON.stringify(config.body), content_type, query: config.query, auth_type: this.options.auth_type, date: date_now, private_key: this.options.api_secret, headers, }); headers["date"] = date_now; } return headers; } private shouldRetry(error: any): boolean { if (error.code === "ECONNABORTED") { return true; } if (error.response && error.response.status >= 500) { return true; } return false; } private delayWithJitter(retry_attempt: number): Promise { const delay = (this.DELAY_BASE * 2) ^ (retry_attempt - 1); // jitter between -halfOfTheDelay seconds and halfOfTheDelay seconds const jitter = delay * (Math.random() - 0.5); // to ensure the delay would not be less than 1 second even if the delayBase is smaller than 2 const totalDelay = Math.max(1, delay + jitter); return new Promise((resolve) => { setTimeout(resolve, totalDelay); }); } private async withRetry( requestConfig: AxiosRequestConfig, retry: number = 0, ): Promise { try { const response = await axios(requestConfig); return response; } catch (error: any) { if (this.shouldRetry(error) && retry < this.options.max_retry) { await this.delayWithJitter(retry); retry++; return this.withRetry(requestConfig, retry); } throw error; } } public async makeRequest(config: RequestConfig): Promise { const headers = this.getHeaders(config); try { const axiosConfig: AxiosRequestConfig = { url: config.url, method: config.method, headers, params: config.query, validateStatus: (status) => status >= 200 && status < 300, baseURL: this.options.domain, data: config.body, timeout: this.options.timeout, proxy: this.options.proxy, }; // Use custom paramsSerializer to match signature calculation encoding (RFC 3986) // Signature calculation uses querystring.escape(), so we need to use the same encoding if (config.query) { axiosConfig.paramsSerializer = (params: any) => { const query_keys = Object.keys(params).sort(); const parts: string[] = []; for (const k of query_keys) { const value = params[k]; if (value !== null && value !== undefined) { const key = k.trim(); const val = value.toString().trim(); // Use querystring.escape (RFC 3986) to match signature calculation parts.push(`${key}=${querystring.escape(val)}`); } } return parts.join("&"); }; } const response = await this.withRetry(axiosConfig); const plainHeaders: Record = {}; if (response.headers) { for (const key in response.headers) { if (Object.prototype.hasOwnProperty.call(response.headers, key)) { const value = response.headers[key]; if (Array.isArray(value)) { plainHeaders[key] = value.join(", "); } else if (value !== null && value !== undefined) { plainHeaders[key] = String(value); } } } } return { response_headers: plainHeaders, data: response.data.data, } as T; } catch (error) { throw this.handleError(error); } } private handleError(e: any): Error { if (e.code === "ECONNABORTED") { return new AftershipError( "Request timed out.", AfterShipErrorCodes.TIMED_OUT, ); } if (e instanceof AftershipError) { return e; } const response = e.response; const response_data_str = JSON.stringify(response?.data); if (response) { const meta = response.data?.meta; const message = meta?.message || e.message; let code = AfterShipMetaCodeMap[meta?.code?.toString()]; if (!code) { if (response.status >= 500) { code = AfterShipErrorCodes.UNKNOWN_ERROR; } else if (response.status >= 400) { code = AfterShipErrorCodes.BAD_REQUEST; } } return new AftershipError( message, code, meta?.code, response.status, response_data_str, response.headers, ); } return new AftershipError(e.message, AfterShipErrorCodes.UNKNOWN_ERROR); } }