UNPKG

@bitrix24/b24jssdk

Version:

Bitrix24 REST API JavaScript SDK

564 lines (561 loc) 18.5 kB
/** * @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