Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Removed dependency on `axios`. The SDK now uses `fetch` API which is available in modern Node.js versions and browsers.
- [BREAKING] - Axios exceptions are no longer thrown. Instead, standard `Error` objects are used.

## [7.0.2] - 2025-07-29

### Fixed
Expand Down
158 changes: 61 additions & 97 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 3 additions & 7 deletions packages/azure-kusto-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,10 @@
"rimraf": "^3.0.2"
},
"dependencies": {
"@azure/core-auth": "^1.10.0",
"@azure/core-util": "^1.10.0",
"@azure/identity": "^4.0.1",
"@types/uuid": "^8.3.4",
"axios": "^1.8.4",
"follow-redirects": "^1.15.1",
"https-browserify": "^1.0.0",
"stream-http": "^3.2.0",
"uuid": "^8.3.2"
"@azure/identity": "^4.11.1",
"uuid": "^11.1.0"
},
"gitHead": "f8a5dae26d6d2ca2ab8b95953bb9b88a02e8e35d"
}
122 changes: 54 additions & 68 deletions packages/azure-kusto-data/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
// Licensed under the MIT License.

import { isNodeLike } from "@azure/core-util";
import axios, { AxiosInstance, AxiosRequestConfig, RawAxiosRequestHeaders } from "axios";
import http from "http";
import https from "https";
import { v4 as uuidv4 } from "uuid";
import { KustoHeaders } from "./clientDetails.js";
import ClientRequestProperties from "./clientRequestProperties.js";
Expand Down Expand Up @@ -36,9 +33,21 @@ export class KustoClient {
defaultDatabase?: string;
endpoints: { [key in ExecutionType]: string };
aadHelper: AadHelper;
axiosInstance: AxiosInstance;
cancelToken = axios.CancelToken.source();
cancelToken = new AbortController();
private _isClosed: boolean = false;
private readonly _baseRequest: RequestInit = {
headers: new Headers({
Accept: "application/json; charset=utf-8",
"Accept-Encoding": "gzip,deflate",
Connection: "Keep-Alive",
}),
method: "POST",
redirect: "manual",
signal: this.cancelToken.signal,

// The keepalive flag is about the request outliving the page. It's not relevant for node, so we only set it for browsers
keepalive: isNodeLike ? undefined : true,
} as const;

constructor(kcsb: string | ConnectionStringBuilder) {
this.connectionString = typeof kcsb === "string" ? new ConnectionStringBuilder(kcsb) : kcsb;
Expand All @@ -58,34 +67,6 @@ export class KustoClient {
[ExecutionType.QueryV1]: `${this.cluster}/v1/rest/query`,
};
this.aadHelper = new AadHelper(this.connectionString);

let headers: RawAxiosRequestHeaders = {
Accept: "application/json",
};

if (isNodeLike) {
headers = {
...headers,
"Accept-Encoding": "gzip,deflate",
Connection: "Keep-Alive",
};
}
const axiosProps: AxiosRequestConfig = {
headers,
validateStatus: (status: number) => status === 200,
maxBodyLength: Infinity,
maxContentLength: Infinity,
maxRedirects: 0,
};
// http and https are Node modules and are not found in browsers
if (isNodeLike) {
// keepAlive pools and reuses TCP connections, so it's faster
axiosProps.httpAgent = new http.Agent({ keepAlive: true });
axiosProps.httpsAgent = new https.Agent({ keepAlive: true });
}
axiosProps.cancelToken = this.cancelToken.token;

this.axiosInstance = axios.create(axiosProps);
}

async execute(db: string | null, query: string, properties?: ClientRequestProperties): Promise<KustoResponseDataSet> {
Expand Down Expand Up @@ -150,6 +131,7 @@ export class KustoClient {

let payload: { db: string; csl: string; properties?: any };
let clientRequestPrefix = "";
let isPayloadStream = false;

const timeout = this._getClientTimeout(executionType, properties);
let payloadContent: any = "";
Expand All @@ -174,14 +156,15 @@ export class KustoClient {
headers["Content-Encoding"] = "gzip";
headers["Content-Type"] = "application/octet-stream";
} else {
headers["Content-Type"] = "application/json";
headers["Content-Type"] = "application/json; charset=utf-8";
}
isPayloadStream = true;
} else if ("blob" in entity) {
payloadContent = {
payloadContent = JSON.stringify({
sourceUri: entity.blob,
};
});
clientRequestPrefix = "KNC.executeStreamingIngestFromBlob;";
headers["Content-Type"] = "application/json";
headers["Content-Type"] = "application/json; charset=utf-8";
} else {
throw new Error("Invalid parameters - expected query or streaming ingest");
}
Expand All @@ -207,7 +190,7 @@ export class KustoClient {
headers.Authorization = authHeader;
}

return this._doRequest(endpoint, executionType, headers, payloadContent, timeout, properties);
return this._doRequest(endpoint, executionType, headers, payloadContent, timeout, properties, isPayloadStream);
}

private getDb(db: string | null) {
Expand All @@ -223,61 +206,64 @@ export class KustoClient {
async _doRequest(
endpoint: string,
executionType: ExecutionType,
headers: { [header: string]: string },
headers: {
[header: string]: string;
},
payload: any,
timeout: number,
properties?: ClientRequestProperties | null,
isPayloadStream: boolean = false,
): Promise<KustoResponseDataSet> {
// replace non-ascii characters with ? in headers
for (const key of Object.keys(headers)) {
headers[key] = headers[key].replace(/[^\x00-\x7F]+/g, "?");
}

const axiosConfig: AxiosRequestConfig = {
headers,
const request = {
...this._baseRequest,
timeout,
body: payload,
headers: { ...this._baseRequest.headers, ...headers },
// Nodejs requires duplex to be set to "half" when using a ReadableStream as request body
duplex: isNodeLike && isPayloadStream ? "half" : undefined,
};

let axiosResponse;
try {
axiosResponse = await this.axiosInstance.post(endpoint, payload, axiosConfig);
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
// Since it's impossible to modify the error request object, the only way to censor the Authorization header is to remove it.
error.request = undefined;
if (error?.config?.headers) {
error.config.headers.Authorization = "<REDACTED>";
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.response?.request?._header) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.response.request._header = "<REDACTED>";
}
if (error.response && error.response.status === 429) {
throw new ThrottlingError("POST request failed with status 429 (Too Many Requests)", error);
}
const response = await fetch(new Request(endpoint, request));

if (!response.ok) {
if (response.status === 429) {
throw new ThrottlingError("Request failed with status 429 (Too Many Requests)", undefined);
}
throw error;
}

return this._parseResponse(axiosResponse.data, executionType, properties, axiosResponse.status);
// handle redirection
if (response.status >= 300 && response.status < 400 && response.headers.has("location")) {
throw new Error(
`Request was redirected with status ${response.status} (${response.statusText}) to ${response.headers.get("location") || "<unknown>"}. This client does not follow redirects.`,
);
}

throw new Error(`Request failed with status ${response.status} (${response.statusText}) - \`${await response.text()}}\`.`);
}
return this._parseResponse(response, executionType, properties);
}

_parseResponse(response: any, executionType: ExecutionType, properties?: ClientRequestProperties | null, status?: number): KustoResponseDataSet {
async _parseResponse(response: Response, executionType: ExecutionType, properties?: ClientRequestProperties | null): Promise<KustoResponseDataSet> {
const { raw } = properties || {};
if (raw === true || executionType === ExecutionType.Ingest) {
return response;
return response.json();
}

let kustoResponse = null;
try {
const json = await response.json();

if (executionType === ExecutionType.Query) {
kustoResponse = new KustoResponseDataSetV2(response as V2Frames);
kustoResponse = new KustoResponseDataSetV2(json as V2Frames);
} else {
kustoResponse = new KustoResponseDataSetV1(response as V1);
kustoResponse = new KustoResponseDataSetV1(json as V1);
}
} catch (ex) {
throw new Error(`Failed to parse response ({${status}}) with the following error [${ex}].`);
throw new Error(`Failed to parse response ({${response.status}} - {${response.statusText}) with the following error [${ex}].`);
}
if (kustoResponse.getErrorsCount().errors > 0) {
throw new Error(`Kusto request had errors. ${kustoResponse.getExceptions()}`);
Expand All @@ -303,7 +289,7 @@ export class KustoClient {

public close(): void {
if (!this._isClosed) {
this.cancelToken.cancel("Client Closed");
this.cancelToken.abort("Client Closed");
}
this._isClosed = true;
}
Expand Down
52 changes: 18 additions & 34 deletions packages/azure-kusto-data/src/cloudSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import axios from "axios";
import { isNodeLike } from "@azure/core-util";

export type CloudInfo = {
Expand All @@ -12,8 +11,6 @@ export type CloudInfo = {
FirstPartyAuthorityUrl: string;
};

const AXIOS_ERR_NETWORK = axios?.AxiosError?.ERR_NETWORK ?? "ERR_NETWORK";

/**
* This class holds data for all cloud instances, and returns the specific data instance by parsing the dns suffix from a URL
*/
Expand Down Expand Up @@ -43,38 +40,25 @@ class CloudSettings {
return this.cloudCache[cacheKey];
}

try {
const response = await axios.get<{ AzureAD: CloudInfo | undefined }>(this.getAuthMetadataEndpointFromClusterUri(kustoUri), {
headers: {
"Cache-Control": "no-cache",
// Disable caching - it's being cached in memory (Service returns max-age).
// The original motivation for this is due to a CORS issue in Ibiza due to a dynamic subdomain.
// The first dynamic subdomain is attached to the cache and for some reason isn't invalidated
// when there is a new subdomain. It causes the request failure due to CORS.
// Example:
// Access to XMLHttpRequest at 'https://safrankecc.canadacentral.kusto.windows.net/v1/rest/auth/metadata' from origin
// 'https://sandbox-46-11.reactblade.portal.azure.net' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a value
// 'https://sandbox-46-10.reactblade.portal.azure.net' that is not equal to the supplied origin.
},
maxRedirects: 0,
});
if (response.status === 200) {
this.cloudCache[cacheKey] = response.data.AzureAD || this.defaultCloudInfo;
} else {
throw new Error(`Kusto returned an invalid cloud metadata response - ${response}`);
}
} catch (ex) {
if (axios.isAxiosError(ex)) {
// Axios library has a bug in browser, not propagating the status code, see: https://github.com/axios/axios/issues/5330
if ((isNodeLike && ex.response?.status === 404) || (!isNodeLike && (!ex.code || ex.code === AXIOS_ERR_NETWORK))) {
// For now as long not all proxies implement the metadata endpoint, if no endpoint exists return public cloud data
this.cloudCache[cacheKey] = this.defaultCloudInfo;
} else {
throw new Error(`Failed to get cloud info for cluster ${kustoUri} - ${ex}`);
}
}
const response = await fetch(this.getAuthMetadataEndpointFromClusterUri(kustoUri), {
method: "GET",
});
let ex;
if (response.status === 200) {
this.cloudCache[cacheKey] = ((await response.json()) as { AzureAD: CloudInfo }).AzureAD;
return this.cloudCache[cacheKey];
} else if (response.status === 404) {
// For now as long not all proxies implement the metadata endpoint, if no endpoint exists return public cloud data
this.cloudCache[cacheKey] = this.defaultCloudInfo;
return this.cloudCache[cacheKey];
} else if (response.status >= 300 && response.status < 400) {
ex = Error(
`Request was redirected with status ${response.status} (${response.statusText}) to ${response.headers.get("location") || "<unknown>"}. This client does not follow redirects.`,
);
} else {
ex = Error(`Kusto returned an invalid cloud metadata response - ${await response.json()}`);
}
return this.cloudCache[cacheKey];
throw new Error(`Failed to get cloud info for cluster ${kustoUri} - ${ex}`);
}

private getCacheKey(kustoUri: string): string {
Expand Down
Loading
Loading