import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from "axios"; import { getVersion } from "./getVersion"; export interface HttpClientOptions { apiKey: string; apiUrl: string; timeoutMs?: number; maxRetries?: number; backoffFactor?: number; // seconds factor for 0.5, 1, 2... } export class HttpClient { private instance: AxiosInstance; private readonly apiKey: string; private readonly apiUrl: string; private readonly maxRetries: number; private readonly backoffFactor: number; constructor(options: HttpClientOptions) { this.apiKey = options.apiKey; this.apiUrl = options.apiUrl.replace(/\/$/, ""); this.maxRetries = options.maxRetries ?? 3; this.backoffFactor = options.backoffFactor ?? 0.5; this.instance = axios.create({ baseURL: this.apiUrl, timeout: options.timeoutMs ?? 300000, headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}`, }, transitional: { clarifyTimeoutError: true }, }); } getApiUrl(): string { return this.apiUrl; } getApiKey(): string { return this.apiKey; } private async request(config: AxiosRequestConfig): Promise> { const version = getVersion(); config.headers = { ...(config.headers || {}), }; let lastError: any; for (let attempt = 0; attempt < this.maxRetries; attempt++) { try { const cfg: AxiosRequestConfig = { ...config }; // For POST/PUT, ensure origin is present in JSON body too if (cfg.method && ["post", "put", "patch"].includes(cfg.method.toLowerCase())) { const data = (cfg.data ?? {}) as Record; cfg.data = { ...data, origin: typeof data.origin === "string" && data.origin.includes("mcp") ? data.origin : `js-sdk@${version}` }; // If timeout is specified in the body, use it to override the request timeout if (typeof data.timeout === "number") { cfg.timeout = data.timeout + 5000; } } const res = await this.instance.request(cfg); if (res.status === 502 && attempt < this.maxRetries - 1) { await this.sleep(this.backoffFactor * Math.pow(2, attempt)); continue; } return res; } catch (err: any) { lastError = err; const status = err?.response?.status; if (status === 502 && attempt < this.maxRetries - 1) { await this.sleep(this.backoffFactor * Math.pow(2, attempt)); continue; } throw err; } } throw lastError ?? new Error("Unexpected HTTP client error"); } private sleep(seconds: number): Promise { return new Promise((r) => setTimeout(r, seconds * 1000)); } post(endpoint: string, body: Record, headers?: Record) { return this.request({ method: "post", url: endpoint, data: body, headers }); } get(endpoint: string, headers?: Record) { return this.request({ method: "get", url: endpoint, headers }); } delete(endpoint: string, headers?: Record) { return this.request({ method: "delete", url: endpoint, headers }); } prepareHeaders(idempotencyKey?: string): Record { const headers: Record = {}; if (idempotencyKey) headers["x-idempotency-key"] = idempotencyKey; return headers; } }