@bitrix24/b24jssdk
Version:
Bitrix24 REST API JavaScript SDK
820 lines (817 loc) • 31.3 kB
JavaScript
/**
* @package @bitrix24/b24jssdk
* @version 3.0.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 { parseErrorPayload } from './parse-error-payload.mjs';
import { AjaxResult } from './ajax-result.mjs';
import { redactSensitiveParams } from './redact.mjs';
import { pickHttpOptions, HTTP_OPTION_KEYS } from './http-options.mjs';
import { Type } from '../../tools/type.mjs';
import { isBrowserLikeRuntime } from '../../tools/environment.mjs';
import { ApiVersion } from '../../types/b24.mjs';
import { SdkError } from '../sdk-error.mjs';
import { LoggerFactory } from '../../logger/logger-factory.mjs';
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
const LOG_MAX_LENGTH = 300;
const LOG_SLICE_LENGTH = 100;
function truncateForLog(value) {
const text = typeof value === "string" ? value : String(value);
return text.length > LOG_MAX_LENGTH ? text.slice(0, LOG_SLICE_LENGTH) + "..." : text;
}
__name(truncateForLog, "truncateForLog");
const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
const IDEMPOTENCY_KEY_HEADER_LOWER = "idempotency-key";
const IDEMPOTENT_REPLAYED_HEADER_LOWER = "idempotent-replayed";
const IDEMPOTENCY_KEY_RE = /^[\u0021-\u007E]{1,255}$/;
function readIdempotencyHeaders(headers) {
if (null === headers || typeof headers !== "object") {
return void 0;
}
let key;
let replayed;
for (const [name, rawValue] of Object.entries(headers)) {
const lowerName = name.toLowerCase();
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
if (IDEMPOTENCY_KEY_HEADER_LOWER === lowerName && typeof value === "string") {
key = value;
} else if (IDEMPOTENT_REPLAYED_HEADER_LOWER === lowerName) {
replayed = true === value || "true" === String(value).trim().toLowerCase();
}
}
if (void 0 === key && void 0 === replayed) {
return void 0;
}
return { key, replayed: replayed ?? false };
}
__name(readIdempotencyHeaders, "readIdempotencyHeaders");
function isCorsEnforcedRuntime() {
return isBrowserLikeRuntime();
}
__name(isCorsEnforcedRuntime, "isCorsEnforcedRuntime");
const reportedHttpOptions = /* @__PURE__ */ new WeakSet();
function preferredAdapter() {
if (!isBrowserLikeRuntime()) {
return {};
}
return "function" === typeof globalThis.fetch ? { adapter: "fetch" } : {};
}
__name(preferredAdapter, "preferredAdapter");
class AbstractHttp {
static {
__name(this, "AbstractHttp");
}
_clientAxios;
_authActions;
_requestIdGenerator;
_restrictionManager;
/**
* In-flight token refresh, shared so concurrent 401s coalesce into a single
* `refreshAuth()` round-trip — avoids OAuth refresh-token reuse errors when a
* burst of requests expires together. (#182)
*/
_pendingRefresh = null;
_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/3.0.0";
}
this._authActions = authActions;
this._requestIdGenerator = new RequestIdGenerator();
const { picked: httpOptions, dropped: droppedHttpOptions } = pickHttpOptions(options);
this._clientAxios = axios.create({
timeout: 3e4,
timeoutErrorMessage: "Request timeout exceeded",
...preferredAdapter(),
...httpOptions,
// headers last so the merged default + caller headers aren't wiped by an
// `options.headers` (or the previous `headers: undefined`) spread (#144).
// Read from `options` rather than from the filtered config: the merge
// predates `TypeHttpOptions` and is left as it was. The SDK's own
// `Content-Type` is decided per request and beats anything set here
// (measured, lowercase spelling included); `Authorization` is per-request
// only on the OAuth header branch, so on a webhook an instance header of
// that name does reach the wire — which is one more reason the type does
// not offer this key.
headers: {
...defaultHeaders,
...options?.headers ?? {}
}
});
if (droppedHttpOptions.length > 0 && !reportedHttpOptions.has(options)) {
reportedHttpOptions.add(options);
LoggerFactory.forcedLog(
this._logger,
"warning",
"httpOptions: keys not accepted at construction were dropped",
{
dropped: droppedHttpOptions.join(", "),
accepted: HTTP_OPTION_KEYS.join(", "),
hint: "set them on getHttpClient(version).ajaxClient.defaults instead"
}
).catch(() => {
});
}
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.totalRequests,
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.totalRequests = 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
* @param options - Per-request transport options (currently `idempotencyKey`)
* @returns Promise with AjaxResult
*/
async call(method, params, requestId, options) {
requestId = requestId ?? this._requestIdGenerator.getRequestId();
const maxRetries = this._restrictionManager.getParams().maxRetries;
this._validateParams(requestId, method, params);
this._logRequest(requestId, method, params);
const requestConfig = this._prepareRequestConfig(requestId, method, options);
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, requestConfig);
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 + 1 < 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.isSoftError(lastError)) {
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) {
const errorCode = `${axiosError.code || "JSSDK_AXIOS_ERROR"}`;
const 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
});
}
const parsed = parseErrorPayload(axiosError.response?.data, errorCode, errorDescription);
return new AjaxError({
code: parsed?.code ?? errorCode,
description: parsed?.description ?? errorDescription,
status,
validation: parsed?.validation,
isV3Envelope: parsed?.isV3Envelope,
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, requestConfig) {
this._checkClientSideWarning(requestId);
const authData = await this._ensureAuth(requestId);
const response = await this._makeRequestWithAuthRetry(requestId, method, params, authData, requestConfig);
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._refreshAuth();
}
return authData;
}
/**
* Refresh the auth token, coalescing concurrent callers onto a single
* in-flight `refreshAuth()` so a burst of 401s triggers exactly one refresh
* round-trip. The slot clears once the refresh settles. (#182)
*/
_refreshAuth() {
if (this._pendingRefresh) {
return this._pendingRefresh;
}
const refresh = this._authActions.refreshAuth();
this._pendingRefresh = refresh;
refresh.finally(() => {
this._pendingRefresh = null;
}).catch(() => {
});
return refresh;
}
// Execute the request with 401 error handling
async _makeRequestWithAuthRetry(requestId, method, params, authData, requestConfig) {
try {
await this._restrictionManager.checkRateLimit(requestId, method);
return await this._makeAxiosRequest(requestId, method, params, authData, requestConfig);
} catch (error) {
if (error instanceof AxiosError) {
this.getLogger().info(
`post/catchError`,
{
requestId,
status: error.status,
// Redact in case a future portal response embeds credentials in
// the error body (today it doesn't, but the channel is open) (#39),
// and cap the length so a large error body can't flood the sink (#236).
responseData: truncateForLog(JSON.stringify(redactSensitiveParams(error?.response?.data), null, 0))
}
).catch(() => {
});
}
const ajaxError = this._convertToAjaxError(requestId, error, method, params);
if (this._isAuthError(ajaxError)) {
this._logAuthErrorDetected(requestId);
this._logRefreshingAuthToken(requestId);
const refreshedAuthData = await this._refreshAuth();
await this._restrictionManager.checkRateLimit(requestId, method);
return await this._makeAxiosRequest(requestId, method, params, refreshedAuthData, requestConfig);
}
throw ajaxError;
}
}
async _makeAxiosRequest(requestId, method, params, authData, requestConfig) {
let methodFormatted = this._prepareMethod(requestId, method, this.getBaseUrl());
const isV3Batch = ApiVersion.v3 === this._version && "batch" === method;
const isBareArrayBody = isV3Batch && Array.isArray(params);
const isHook = "hook" === authData.refresh_token;
const hasAccessToken = "string" === typeof authData.access_token && authData.access_token.trim().length > 0;
const hasIdempotencyKey = ApiVersion.v3 === this._version && "string" === typeof requestConfig?.headers?.[IDEMPOTENCY_KEY_HEADER];
const authOutOfBody = (isBareArrayBody || hasIdempotencyKey) && !isHook && hasAccessToken;
const canSendAuthHeader = authOutOfBody && !isCorsEnforcedRuntime();
const useQueryAuth = authOutOfBody && isCorsEnforcedRuntime();
const sendBareArray = isBareArrayBody && (isHook || canSendAuthHeader || useQueryAuth);
const paramsFormatted = sendBareArray ? params : this._prepareParams(authData, params);
if (authOutOfBody && !sendBareArray) {
delete paramsFormatted.auth;
}
const jsonBody = { "Content-Type": "application/json" };
const effectiveConfig = canSendAuthHeader ? {
maxRedirects: 0,
...requestConfig,
headers: {
...jsonBody,
...requestConfig?.headers,
Authorization: `Bearer ${authData.access_token}`
}
} : useQueryAuth ? {
maxRedirects: 0,
...requestConfig,
headers: { ...jsonBody, ...requestConfig?.headers }
} : {
...requestConfig,
headers: { ...jsonBody, ...requestConfig?.headers }
};
const paramsFormattedForLog = JSON.stringify(redactSensitiveParams(paramsFormatted), null, 0);
this.getLogger().info(
`post/send`,
{
requestId,
method,
params: truncateForLog(paramsFormattedForLog)
}
).catch(() => {
});
if (useQueryAuth) {
const separator = methodFormatted.includes("?") ? "&" : "?";
methodFormatted += `${separator}auth=${encodeURIComponent(authData.access_token)}`;
}
const response = await this._clientAxios.post(methodFormatted, paramsFormatted, effectiveConfig);
const effectiveMaxRedirects = effectiveConfig?.maxRedirects ?? this._clientAxios.defaults.maxRedirects;
if (0 === effectiveMaxRedirects && 0 === response.status) {
throw new AjaxError({
code: "JSSDK_HTTP_REDIRECT_BLOCKED",
description: "This request does not follow redirects (`maxRedirects: 0`) and the answer carried no status of its own \u2014 which is what a refused redirect looks like on the fetch adapter, and what a dropped connection can look like too. The SDK sets `maxRedirects: 0` on a `restApi:v3` batch and on a `restApi:v3` call with `idempotencyKey`, both on a non-hook transport, because they carry an access token a redirect would take along. Point the SDK at the final URL instead.",
status: 0,
requestInfo: { method, params, requestId }
});
}
const resultFormattedForLog = JSON.stringify(redactSensitiveParams(response.data?.result), null, 0);
this.getLogger().info(
`post/response`,
{
requestId,
// responseFull: JSON.stringify(response.data, null, 2),
result: truncateForLog(resultFormattedForLog),
time: JSON.stringify(response.data?.time, null, 0)
}
).catch(() => {
});
const idempotency = readIdempotencyHeaders(response.headers);
return {
status: response.status,
payload: response.data,
...idempotency ? { idempotency } : {}
};
}
/**
* Builds the per-request axios config for one call, or `undefined` when the
* call needs none.
*
* A `protected` hook rather than a branch inside `_makeAxiosRequest` because
* the two transports genuinely disagree about one option: `HttpV2` overrides
* it to drop `idempotencyKey`, since the v2 endpoint ignores the header and a
* key that is silently dropped leaves a caller believing a retry is
* deduplicated when it is not. It is the mechanism for that disagreement, not
* a speculative extension point — a subclass overriding it owes the same
* contract: return per-request axios config, or `undefined` for none.
*
* @throws {SdkError} `JSSDK_HTTP_INVALID_IDEMPOTENCY_KEY` when the key is not
* 1-255 printable ASCII characters.
* @throws {SdkError} `JSSDK_HTTP_IDEMPOTENCY_KEY_BROWSER` when a v3 call with a
* key runs where CORS applies (#573).
*/
_prepareRequestConfig(_requestId, _method, options) {
const idempotencyKey = options?.idempotencyKey;
if (void 0 === idempotencyKey) {
return void 0;
}
if (!IDEMPOTENCY_KEY_RE.test(idempotencyKey)) {
throw new SdkError({
code: "JSSDK_HTTP_INVALID_IDEMPOTENCY_KEY",
description: "`idempotencyKey` must be 1-255 printable ASCII characters with no whitespace or control characters. A `crypto.randomUUID()` value satisfies this. See https://apidocs.bitrix24.ru/api-reference/rest-v3.html",
status: 500
});
}
if (ApiVersion.v3 === this._version && isCorsEnforcedRuntime()) {
throw new SdkError({
code: "JSSDK_HTTP_IDEMPOTENCY_KEY_BROWSER",
description: "`idempotencyKey` cannot be used from a browser: the portal's CORS preflight does not allow the `Idempotency-Key` header, so a cross-origin request carrying it is refused. Make idempotent writes from a server. See https://github.com/bitrix24/b24jssdk/issues/573",
status: 500
});
}
return { headers: { [IDEMPOTENCY_KEY_HEADER]: idempotencyKey } };
}
_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,
idempotency: response.idempotency
});
if (result.isSuccess) {
const time = result.getData()?.time;
if (time) {
await this._restrictionManager.updateStats(requestId, method, time);
}
}
return result;
}
/**
* Turns an error the transport already built into the soft `AjaxResult` a
* caller receives, for the codes in `RestrictionManager.exceptionCodeForSoft`.
*
* It used to rebuild the error from a synthetic answer holding only `code` and
* `message`, so the portal's real body was discarded here — which is why
* `validation` was unreachable even though `_convertAxiosErrorToAjaxError` had
* just parsed it (#423).
*
* The error is now **carried** rather than re-derived: the synthetic `answer`
* is kept so `_data` still describes the failure for anything reading it, but
* it is no longer what produces the error — which also means the two can no
* longer disagree. `validation` is deliberately not copied into that synthetic
* answer: nothing parses it back out, so it would be dead weight that a future
* refactor could mistake for the source of truth.
*
* The carried error keeps its `originalError` — the raw `AxiosError`, whose
* `config.url` holds the webhook secret. It is non-enumerable (see
* `SdkError`), so spreads and `JSON.stringify` still cannot reach it, but it
* is now readable via `result.getErrors()` on this path as well as on the
* throwing one. That is deliberate: the two paths differ only in how the error
* is delivered, and a caller debugging one should not find less on the other.
*/
_createAjaxResultWithErrorFromResponse(ajaxError, requestId, method, params) {
return new AjaxResult({
answer: {
error: {
code: ajaxError.code,
message: ajaxError.message
}
},
query: { method, params, requestId },
status: ajaxError.status,
// The error itself, not a reconstruction: it was parsed from the portal's
// body a moment ago, and re-deriving it here would fold the validation
// messages onto a description that already holds them (#423).
error: ajaxError
});
}
// endregion ////
// endregion ////
// region Prepare ////
/**
* Builds the request URL: the method path plus the SDK telemetry query params
* (`bx24_request_id` / `bx24_sdk_ver` / `bx24_sdk_type` — request tracing and
* SDK identification, not auth material).
*
* Carve-out for the legacy positional `task.*` methods (`task.commentitem.*`,
* `task.checklistitem.*`, `task.elapseditem.*`, …): these read the request
* **query string positionally**, so appending the telemetry params shifts
* `Param #0` and the server rejects the call —
* `WRONG_ARGUMENTS: Param #0 (taskId) ... expected integer, but given
* something else`. Verified live against a portal: the same
* `task.commentitem.getlist` / `task.checklistitem.getlist` call succeeds
* without the telemetry params and fails with them; modern `tasks.task.*`
* (named params) is unaffected. So telemetry is omitted only for methods whose
* name STARTS WITH `task.`.
*
* Shared by v2 and v3 (rather than per-transport): once the v3 method
* allowlist was dropped (#259) a positional `task.*` method can be routed via
* `actions.v3.*` too, so v3 needs the same suppression — keeping the rule in
* one place stops the two transports drifting apart again (#207).
*
* The match is anchored (`^task\.`): only legacy positional `task.*` methods
* are suppressed. Modern named-param methods `tasks.task.*` / `bizproc.task.*`
* do NOT start with `task.`, so they KEEP telemetry and stay traceable — the
* boundary was pinned live in #271/#272 (`tasks.task.list` works WITH
* telemetry; legacy `task.*` breaks WITH it). Bitrix24 method names are
* lowercase by convention, so the case-sensitive match is sufficient.
*
* @see https://apidocs.bitrix24.com/settings/how-to-call-rest-api/data-encoding.html#order-of-parameters
*/
_prepareMethod(requestId, method, baseUrl) {
const methodUrl = `/${encodeURIComponent(method)}`;
if (/^task\./.test(method)) {
return `${baseUrl}${methodUrl}`;
}
const queryParams = new URLSearchParams({
[this._requestIdGenerator.getQueryStringParameterName()]: requestId,
[this._requestIdGenerator.getQueryStringSdkParameterName()]: "3.0.0",
[this._requestIdGenerator.getQueryStringSdkTypeParameterName()]: "b24-js-sdk"
});
return `${baseUrl}${methodUrl}?${queryParams.toString()}`;
}
/**
* 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 running outside a browser-like runtime — that is,
* on a server. The inverse of {@link isBrowserLikeRuntime}, and a worker is
* **not** server-side: it has no DOM, but the browser's rules apply to it.
*
* @return {boolean}
* @protected
*/
isServerSide() {
return !isBrowserLikeRuntime();
}
/**
* Get the BX24 account address with the path based on the API version
*/
getBaseUrl() {
return this._authActions.getTargetOriginWithPath().get(this._version);
}
// endregion ////
// region Log ////
/**
* Redaction contract: runs caller params through {@link redactSensitiveParams}
* (see `redact.ts`) so credential-bearing keys are masked before they reach any
* logger context. (#39, #73)
* @see redactSensitiveParams
*/
_sanitizeParams(params) {
return redactSensitiveParams(params);
}
/**
* Redaction contract: params are redacted via {@link _sanitizeParams} →
* {@link redactSensitiveParams} before logging. (#73)
* @see redactSensitiveParams
*/
_logRequest(requestId, method, params) {
this.getLogger().debug(`http request starting`, {
requestId,
method,
params: this._sanitizeParams(params),
api: this.apiVersion,
timestamp: Date.now()
}).catch(() => {
});
}
_logAttempt(requestId, method, attempt, maxRetries) {
this.getLogger().info(`http request attempt`, {
requestId,
method,
api: this.apiVersion,
attempt: {
current: attempt,
max: maxRetries
}
}).catch(() => {
});
}
_logRefreshingAuthToken(requestId) {
this.getLogger().info(`http refreshing auth token`, {
requestId,
api: this.apiVersion
}).catch(() => {
});
}
_logAuthErrorDetected(requestId) {
this.getLogger().info(`http auth error detected`, {
requestId,
api: this.apiVersion
}).catch(() => {
});
}
_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))
}
}).catch(() => {
});
}
_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
}
}).catch(() => {
});
}
_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
}
}
).catch(() => {
});
}
_logAllAttemptsExhausted(requestId, method, attempt, maxRetries) {
this.getLogger().warning(`http all retry attempts exhausted`, {
requestId,
method,
api: this.apiVersion,
attempt: {
current: attempt,
max: maxRetries
}
}).catch(() => {
});
}
_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()
}).catch(() => {
});
}
_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) + "%" : "??"
}).catch(() => {
});
}
// 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, IDEMPOTENCY_KEY_HEADER, readIdempotencyHeaders, truncateForLog };
//# sourceMappingURL=abstract-http.mjs.map