UNPKG

@fairmint/canton-node-sdk

Version:
185 lines 8.76 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HttpClient = void 0; const axios_1 = __importDefault(require("axios")); const errors_1 = require("../errors"); /** Handles HTTP requests with authentication, logging, and error handling */ class HttpClient { constructor(logger) { this.axiosInstance = axios_1.default.create(); this.logger = logger; } async makeGetRequest(url, config = {}, _retryCount = 0) { try { const headers = this.buildHeaders(config); const response = await this.axiosInstance.get(url, { headers }); await this.logRequestResponse(url, { method: 'GET', headers }, response.data); return response.data; } catch (error) { // Attempt up to 3 retries for transient errors if (_retryCount < 3 && this.isRetryableError(error)) { await this.logRequestResponse(url, { method: 'GET', retry: _retryCount + 1 }, `Retrying after error (attempt ${_retryCount + 1}/3): ${axios_1.default.isAxiosError(error) ? (error.response?.status ?? 'network error') : String(error)}`); await this.sleep(6000); return this.makeGetRequest(url, config, _retryCount + 1); } // Log the error response before throwing if (axios_1.default.isAxiosError(error)) { await this.logRequestResponse(url, { method: 'GET' }, error.response?.data ?? error.message); } throw this.handleRequestError(error); } } async makePostRequest(url, data, config = {}, _retryCount = 0) { try { const headers = this.buildHeaders(config); const response = await this.axiosInstance.post(url, data, { headers }); await this.logRequestResponse(url, { method: 'POST', headers, data }, response.data); return response.data; } catch (error) { if (_retryCount < 3 && this.isRetryableError(error)) { await this.logRequestResponse(url, { method: 'POST', retry: _retryCount + 1, data }, `Retrying after error (attempt ${_retryCount + 1}/3): ${axios_1.default.isAxiosError(error) ? (error.response?.status ?? 'network error') : String(error)}`); await this.sleep(6000); const retryData = this.prepareDataForRetry(data); return this.makePostRequest(url, retryData, config, _retryCount + 1); } // Log the error response before throwing if (axios_1.default.isAxiosError(error)) { await this.logRequestResponse(url, { method: 'POST', data }, error.response?.data ?? error.message); } throw this.handleRequestError(error); } } async makeDeleteRequest(url, config = {}, _retryCount = 0) { try { const headers = this.buildHeaders(config); const response = await this.axiosInstance.delete(url, { headers }); await this.logRequestResponse(url, { method: 'DELETE', headers }, response.data); return response.data; } catch (error) { if (_retryCount < 3 && this.isRetryableError(error)) { await this.logRequestResponse(url, { method: 'DELETE', retry: _retryCount + 1 }, `Retrying after error (attempt ${_retryCount + 1}/3): ${axios_1.default.isAxiosError(error) ? (error.response?.status ?? 'network error') : String(error)}`); await this.sleep(6000); return this.makeDeleteRequest(url, config, _retryCount + 1); } // Log the error response before throwing if (axios_1.default.isAxiosError(error)) { await this.logRequestResponse(url, { method: 'DELETE' }, error.response?.data ?? error.message); } throw this.handleRequestError(error); } } async makePatchRequest(url, data, config = {}, _retryCount = 0) { try { const headers = this.buildHeaders(config); const response = await this.axiosInstance.patch(url, data, { headers }); await this.logRequestResponse(url, { method: 'PATCH', headers, data }, response.data); return response.data; } catch (error) { if (_retryCount < 3 && this.isRetryableError(error)) { await this.logRequestResponse(url, { method: 'PATCH', retry: _retryCount + 1, data }, `Retrying after error (attempt ${_retryCount + 1}/3): ${axios_1.default.isAxiosError(error) ? (error.response?.status ?? 'network error') : String(error)}`); await this.sleep(6000); const retryData = this.prepareDataForRetry(data); return this.makePatchRequest(url, retryData, config, _retryCount + 1); } // Log the error response before throwing if (axios_1.default.isAxiosError(error)) { await this.logRequestResponse(url, { method: 'PATCH', data }, error.response?.data ?? error.message); } throw this.handleRequestError(error); } } buildHeaders(config) { const headers = {}; if (config.contentType) { headers['Content-Type'] = config.contentType; } else { headers['Content-Type'] = 'application/json'; } if (config.includeBearerToken) { // This will be set by the client that uses this HTTP client // The bearer token should be passed in the config or set separately } return headers; } setBearerToken(token) { this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${token}`; } clearBearerToken() { delete this.axiosInstance.defaults.headers.common['Authorization']; } async logRequestResponse(url, request, response) { if (this.logger) { await this.logger.logRequestResponse(url, request, response); } } handleRequestError(error) { if (axios_1.default.isAxiosError(error)) { const status = error.response?.status; const data = error.response?.data ?? {}; const { code } = data; const msg = code ? `HTTP ${status}: ${code}` : `HTTP ${status}`; const err = new errors_1.ApiError(msg, status, error.response?.statusText); err.response = data; return err; } return new errors_1.NetworkError(`Request failed: ${error instanceof Error ? error.message : String(error)}`); } /** * Determines whether a request error is retryable. Retries on: * * - HTTP 5xx server errors * - Network errors * - Canton-specific transient errors: UNKNOWN_CONTRACT_SYNCHRONIZERS (400), SEQUENCER_BACKPRESSURE (409), HTTP 503 */ isRetryableError(error) { if (axios_1.default.isAxiosError(error)) { const status = error.response?.status; const data = error.response?.data ?? {}; const { code } = data; // Retry on undefined status (network error) if (status === undefined) { return true; } // Retry on 5xx server errors if (status >= 500 && status < 600) { return true; } // Retry on Canton-specific transient errors if (status === 400 && code === 'UNKNOWN_CONTRACT_SYNCHRONIZERS') { return true; } if (status === 409 && code === 'SEQUENCER_BACKPRESSURE') { return true; } return false; } // Only retry non-Axios errors that are instances of NetworkError return error instanceof errors_1.NetworkError; } /** Sleep for the specified number of milliseconds */ async sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Prepares request data for retry by updating commandId fields to avoid duplicate command rejection. If the data * contains a commandId field, appends a retry suffix with timestamp to make it unique. */ prepareDataForRetry(data) { if (data && typeof data === 'object' && 'commandId' in data) { const originalCommandId = data.commandId; const retryCommandId = `${originalCommandId}-retry`; return { ...data, commandId: retryCommandId }; } return data; } } exports.HttpClient = HttpClient; //# sourceMappingURL=HttpClient.js.map