@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
564 lines (561 loc) • 18.5 kB
JavaScript
/**
* @package @bitrix24/b24jssdk
* @version 1.1.0
* @copyright (c) 2026 Bitrix24
* @license MIT
* @see https://github.com/bitrix24/b24jssdk
* @see https://bitrix24.github.io/b24jssdk/
*/
import axios, { AxiosError } from 'axios';
import { RequestIdGenerator } from '../request-id-generator.mjs';
import { ParamsFactory } from './limiters/params-factory.mjs';
import { RestrictionManager } from './limiters/manager.mjs';
import { AjaxError } from './ajax-error.mjs';
import { AjaxResult } from './ajax-result.mjs';
import { Type } from '../../tools/type.mjs';
import { getEnvironment, Environment } from '../../tools/environment.mjs';
import { ApiVersion } from '../../types/b24.mjs';
import { LoggerFactory } from '../../logger/logger-factory.mjs';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
class AbstractHttp {
static {
__name(this, "AbstractHttp");
}
_clientAxios;
_authActions;
_requestIdGenerator;
_restrictionManager;
_logger;
_isClientSideWarning = false;
_clientSideWarningMessage = "";
_version;
_metrics = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
totalDuration: 0,
byMethod: /* @__PURE__ */ new Map(),
lastErrors: []
};
constructor(authActions, options, restrictionParams) {
this._version = ApiVersion.v2;
this._logger = LoggerFactory.createNullLogger();
const defaultHeaders = {};
if (this.isServerSide()) {
defaultHeaders["User-Agent"] = "b24-js-sdk/1.1.0";
}
this._authActions = authActions;
this._requestIdGenerator = new RequestIdGenerator();
this._clientAxios = axios.create({
headers: {
...defaultHeaders,
...options ? options.headers : {}
},
timeout: 3e4,
timeoutErrorMessage: "Request timeout exceeded",
...options && { ...options, headers: void 0 }
});
const params = {
...ParamsFactory.getDefault(),
...restrictionParams
};
this._restrictionManager = new RestrictionManager(params);
}
get apiVersion() {
return this._version;
}
get ajaxClient() {
return this._clientAxios;
}
// region Logger ////
setLogger(logger) {
this._logger = logger;
this._restrictionManager.setLogger(this._logger);
}
getLogger() {
return this._logger;
}
// endregion ////
// region RestrictionManager ////
async setRestrictionManagerParams(params) {
await this._restrictionManager.setConfig(params);
}
getRestrictionManagerParams() {
return this._restrictionManager.getParams();
}
/**
* @inheritDoc
*/
getStats() {
return {
...this._restrictionManager.getStats(),
totalRequests: this._metrics.totalDuration,
successfulRequests: this._metrics.successfulRequests,
failedRequests: this._metrics.failedRequests,
totalDuration: this._metrics.totalDuration,
byMethod: this._metrics.byMethod,
lastErrors: this._metrics.lastErrors
};
}
/**
* @inheritDoc
*/
async reset() {
this._metrics.totalDuration = 0;
this._metrics.successfulRequests = 0;
this._metrics.failedRequests = 0;
this._metrics.totalDuration = 0;
this._metrics.byMethod.clear();
this._metrics.lastErrors = [];
return this._restrictionManager.reset();
}
// endregion ////
// region Metrics ////
_updateMetrics(method, isSuccess, duration, error) {
this._metrics.totalRequests++;
if (isSuccess) {
this._metrics.successfulRequests++;
} else {
this._metrics.failedRequests++;
if (error instanceof AjaxError) {
this._metrics.lastErrors.push({
method,
error: error.message,
timestamp: Date.now()
});
if (this._metrics.lastErrors.length > 100) {
this._metrics.lastErrors = this._metrics.lastErrors.slice(-100);
}
}
}
if (!this._metrics.byMethod.has(method)) {
this._metrics.byMethod.set(method, { count: 0, totalDuration: 0 });
}
const methodMetrics = this._metrics.byMethod.get(method);
methodMetrics.count++;
methodMetrics.totalDuration += duration;
}
// endregion ////
_validateParams(requestId, method, params) {
try {
JSON.stringify(params);
} catch (error) {
throw new AjaxError({
code: "JSSDK_INVALID_PARAMS",
description: "Parameters contain circular references",
status: 400,
requestInfo: { method, params, requestId },
originalError: error
});
}
}
/**
* Calling the RestApi function
* @param method - REST API method name
* @param params - Parameters for the method.
* @param requestId - Request id
* @returns Promise with AjaxResult
*/
async call(method, params, requestId) {
requestId = requestId ?? this._requestIdGenerator.getRequestId();
const maxRetries = this._restrictionManager.getParams().maxRetries;
this._validateParams(requestId, method, params);
this._logRequest(requestId, method, params);
let lastError = null;
const startTime = Date.now();
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
this._logAttempt(requestId, method, attempt + 1, maxRetries);
await this._restrictionManager.applyOperatingLimits(requestId, method, params);
const result = await this._executeSingleCall(requestId, method, params);
const duration = Date.now() - startTime;
this._restrictionManager.resetErrors(method);
this._updateMetrics(method, true, duration);
this._logSuccessfulRequest(requestId, method, duration);
return result;
} catch (error) {
lastError = this._convertToAjaxError(requestId, error, method, params);
const duration = Date.now() - startTime;
this._restrictionManager.incrementError(method);
this._updateMetrics(method, false, duration, lastError);
this._logFailedRequest(requestId, method, attempt + 1, maxRetries, lastError);
if (attempt < maxRetries) {
const waitTime = await this._restrictionManager.handleError(requestId, method, params, lastError, attempt);
if (waitTime > 0) {
this._restrictionManager.incrementStats("limitHits");
this._logAttemptRetryWaiteDelay(requestId, method, waitTime, attempt + 1, maxRetries);
await this._restrictionManager.waiteDelay(waitTime);
this._restrictionManager.incrementStats("retries");
continue;
}
}
if (attempt + 1 === maxRetries) {
this._logAllAttemptsExhausted(requestId, method, attempt + 1, maxRetries);
}
if (this._restrictionManager.exceptionCodeForSoft.includes(lastError.code)) {
return this._createAjaxResultWithErrorFromResponse(lastError, requestId, method, params);
}
throw lastError;
}
}
throw new AjaxError({
code: "JSSDK_CALL_ALL_ATTEMPTS_EXHAUSTED",
description: "All attempts exhausted",
status: lastError?.status || 500,
requestInfo: { method, params, requestId },
originalError: lastError?.originalError || null
});
}
_convertToAjaxError(requestId, error, method, params) {
if (error instanceof AjaxError) {
return error;
}
if (error instanceof AxiosError) {
return this._convertAxiosErrorToAjaxError(requestId, error, method, params);
}
return this._convertUnknownErrorToAjaxError(requestId, error, method, params);
}
_convertAxiosErrorToAjaxError(requestId, axiosError, method, params) {
let errorCode = `${axiosError.code || "JSSDK_AXIOS_ERROR"}`;
let errorDescription = axiosError.message;
const status = axiosError.response?.status || 0;
if (errorCode === "ERR_NETWORK") {
return new AjaxError({
code: "NETWORK_ERROR",
description: "Network connection failed",
status: 0,
requestInfo: { method, params, requestId },
originalError: axiosError
});
}
if (errorCode === "ECONNABORTED" || axiosError.message.includes("timeout")) {
return new AjaxError({
code: "REQUEST_TIMEOUT",
description: "Request timeout exceeded",
status: 408,
requestInfo: { method, params, requestId },
originalError: axiosError
});
}
if (axiosError.response?.data && typeof axiosError.response.data === "object") {
const responseData = axiosError.response.data;
if (responseData.error && typeof responseData.error === "object" && "code" in responseData.error) {
errorCode = responseData.error.code;
errorDescription = responseData.error.message.trimEnd();
if (responseData.error.validation) {
if (errorDescription.length > 0) {
if (!errorDescription.endsWith(".")) {
errorDescription += `.`;
}
errorDescription += ` `;
}
responseData.error.validation.forEach((row) => {
errorDescription += `${row?.message || JSON.stringify(row)}`;
});
}
} else if (responseData.error && typeof responseData.error === "string") {
errorCode = responseData.error !== "0" ? responseData.error : errorCode;
errorDescription = responseData?.error_description ?? errorDescription;
}
}
return new AjaxError({
code: errorCode,
description: errorDescription,
status,
requestInfo: { method, params, requestId },
originalError: axiosError
});
}
_convertUnknownErrorToAjaxError(requestId, error, method, params) {
return new AjaxError({
code: "JSSDK_UNKNOWN_ERROR",
description: error instanceof Error ? error.message : String(error),
status: 0,
requestInfo: { method, params, requestId },
originalError: error
});
}
// region Execute Single Call ////
/**
* Performs a single call with
* - 401 error handling
* - rate limit check
* - updating operating statistics
*/
async _executeSingleCall(requestId, method, params) {
this._checkClientSideWarning(requestId);
const authData = await this._ensureAuth(requestId);
const response = await this._makeRequestWithAuthRetry(requestId, method, params, authData);
return this._createAjaxResultFromResponse(response, requestId, method, params);
}
// Get/update authorization
async _ensureAuth(requestId) {
let authData = this._authActions.getAuthData();
if (authData === false) {
this._logRefreshingAuthToken(requestId);
authData = await this._authActions.refreshAuth();
}
return authData;
}
// Execute the request with 401 error handling
async _makeRequestWithAuthRetry(requestId, method, params, authData) {
try {
await this._restrictionManager.checkRateLimit(requestId, method);
return await this._makeAxiosRequest(requestId, method, params, authData);
} catch (error) {
if (error instanceof AxiosError) {
this.getLogger().info(
`post/catchError`,
{
requestId,
status: error.status,
responseData: JSON.stringify(error?.response?.data, null, 0)
}
);
}
if (this._isAuthError(error)) {
this._logAuthErrorDetected(requestId);
this._logRefreshingAuthToken(requestId);
const refreshedAuthData = await this._authActions.refreshAuth();
await this._restrictionManager.checkRateLimit(requestId, method);
return await this._makeAxiosRequest(requestId, method, params, refreshedAuthData);
}
throw error;
}
}
async _makeAxiosRequest(requestId, method, params, authData) {
const methodFormatted = this._prepareMethod(requestId, method, this.getBaseUrl());
const paramsFormatted = this._prepareParams(authData, params);
const paramsFormattedForLog = JSON.stringify(paramsFormatted, null, 0);
const maxLogLength = 300;
const sliceLogLength = 100;
this.getLogger().info(
`post/send`,
{
requestId,
method: methodFormatted,
params: paramsFormattedForLog.length > maxLogLength ? paramsFormattedForLog.slice(0, sliceLogLength) + "..." : paramsFormattedForLog
}
);
const response = await this._clientAxios.post(methodFormatted, paramsFormatted);
const resultFormattedForLog = JSON.stringify(response.data.result, null, 0);
this.getLogger().info(
`post/response`,
{
requestId,
// responseFull: JSON.stringify(response.data, null, 2),
result: resultFormattedForLog.length > maxLogLength ? resultFormattedForLog.slice(0, sliceLogLength) + "..." : resultFormattedForLog,
time: JSON.stringify(response.data.time, null, 0)
}
);
return {
status: response.status,
payload: response.data
};
}
_isAuthError(error) {
if (!(error instanceof AjaxError)) {
return false;
}
return error.status === 401 && ["expired_token", "invalid_token"].includes(error.code);
}
async _createAjaxResultFromResponse(response, requestId, method, params) {
const result = new AjaxResult({
answer: response.payload,
query: { method, params, requestId },
status: response.status
});
if (result.isSuccess) {
const time = result.getData()?.time;
await this._restrictionManager.updateStats(requestId, method, time);
}
return result;
}
/**
* This works in conjunction with the AbstractHttp._convertAxiosErrorToAjaxError function
*/
_createAjaxResultWithErrorFromResponse(ajaxError, requestId, method, params) {
return new AjaxResult({
answer: {
error: {
code: ajaxError.code,
message: ajaxError.message
}
},
query: { method, params, requestId },
status: ajaxError.status
});
}
/**
* Processes function parameters and adds authorization
*/
_prepareParams(authData, params) {
const result = { ...params };
if (authData.refresh_token !== "hook") {
result.auth = authData.access_token;
}
if (result?.data && "start" in result.data) {
const { start, ...dataWithoutStart } = result.data;
result.data = dataWithoutStart;
}
return result;
}
/**
* @inheritDoc
*/
setClientSideWarning(value, message) {
this._isClientSideWarning = value;
this._clientSideWarningMessage = message;
}
// endregion ////
// region Tools ////
/**
* Tests whether the code is executed on the client side
* @return {boolean}
* @protected
*/
isServerSide() {
return getEnvironment() !== Environment.BROWSE;
}
/**
* Get the BX24 account address with the path based on the API version
*/
getBaseUrl() {
return this._authActions.getTargetOriginWithPath().get(this._version);
}
// endregion ////
// region Log ////
_sanitizeParams(params) {
const sanitized = { ...params };
const sensitiveKeys = ["auth", "password", "token", "secret", "access_token", "refresh_token"];
sensitiveKeys.forEach((key) => {
if (key in sanitized && sanitized[key]) {
sanitized[key] = "***REDACTED***";
}
});
return sanitized;
}
_logRequest(requestId, method, params) {
this.getLogger().debug(`http request starting`, {
requestId,
method,
params: this._sanitizeParams(params),
api: this.apiVersion,
timestamp: Date.now()
});
}
_logAttempt(requestId, method, attempt, maxRetries) {
this.getLogger().info(`http request attempt`, {
requestId,
method,
api: this.apiVersion,
attempt: {
current: attempt,
max: maxRetries
}
});
}
_logRefreshingAuthToken(requestId) {
this.getLogger().info(`http refreshing auth token`, {
requestId,
api: this.apiVersion
});
}
_logAuthErrorDetected(requestId) {
this.getLogger().info(`http auth error detected`, {
requestId,
api: this.apiVersion
});
}
_logSuccessfulRequest(requestId, method, duration) {
this.getLogger().debug(`http request successful`, {
requestId,
method,
api: this.apiVersion,
duration: {
ms: duration,
sec: Number.parseFloat((duration / 1e3).toFixed(2))
}
});
}
_logFailedRequest(requestId, method, attempt, maxRetries, error) {
this.getLogger().debug(`http request failed`, {
requestId,
method,
api: this.apiVersion,
attempt: {
current: attempt,
max: maxRetries
},
error: {
code: error.code,
message: error.message,
status: error.status
}
});
}
_logAttemptRetryWaiteDelay(requestId, method, wait, attempt, maxRetries) {
this.getLogger().debug(
`http wait ${(wait / 1e3).toFixed(2)} sec.`,
{
requestId,
method,
api: this.apiVersion,
wait,
attempt: {
current: attempt,
max: maxRetries
}
}
);
}
_logAllAttemptsExhausted(requestId, method, attempt, maxRetries) {
this.getLogger().warning(`http all retry attempts exhausted`, {
requestId,
method,
api: this.apiVersion,
attempt: {
current: attempt,
max: maxRetries
}
});
}
_logBatchStart(requestId, calls, options) {
const callCount = Array.isArray(calls) ? calls.length : Object.keys(calls).length;
this.getLogger().debug(`http batch request starting `, {
requestId,
callCount,
api: this.apiVersion,
isHaltOnError: options.isHaltOnError,
timestamp: Date.now()
});
}
_logBatchCompletion(requestId, total, errors) {
this.getLogger().debug(`http batch request completed`, {
requestId,
api: this.apiVersion,
totalCalls: total,
successful: total - errors,
failed: errors,
successRate: total > 0 ? ((total - errors) / total * 100).toFixed(1) + "%" : "??"
});
}
// Check client-side warnings
_checkClientSideWarning(requestId) {
if (this._isClientSideWarning && !this.isServerSide() && Type.isStringFilled(this._clientSideWarningMessage)) {
LoggerFactory.forcedLog(
this.getLogger(),
"warning",
this._clientSideWarningMessage,
{
requestId,
code: "JSSDK_CLIENT_SIDE_WARNING"
}
);
}
}
// endregion ////
}
export { AbstractHttp };
//# sourceMappingURL=abstract-http.mjs.map