@fairmint/canton-node-sdk
Version:
Canton Node SDK
185 lines • 8.76 kB
JavaScript
"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