Merge branch 'master' into quantized_vector_playwright

This commit is contained in:
sunghyunkang1111
2026-06-04 10:28:32 -05:00
committed by GitHub
19 changed files with 523 additions and 57 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ module.exports = {
}, },
], ],
rules: { rules: {
"no-console": ["error", { allow: ["error", "warn", "dir"] }], //CTODO uncomment when console debugging is reverted: "no-console": ["error", { allow: ["error", "warn", "dir"] }],
curly: "error", curly: "error",
"@typescript-eslint/switch-exhaustiveness-check": "error", "@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": "error",
+16 -13
View File
@@ -76,7 +76,7 @@
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "23.11.5", "i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "3.0.2", "i18next-http-backend": "3.0.5",
"i18next-resources-to-backend": "1.2.1", "i18next-resources-to-backend": "1.2.1",
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
"immer": "9.0.6", "immer": "9.0.6",
@@ -12724,17 +12724,19 @@
} }
}, },
"node_modules/cross-fetch": { "node_modules/cross-fetch": {
"version": "4.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.12" "node-fetch": "^2.7.0"
} }
}, },
"node_modules/cross-fetch/node_modules/node-fetch": { "node_modules/cross-fetch/node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
@@ -15908,7 +15910,6 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": [
@@ -16918,11 +16919,12 @@
} }
}, },
"node_modules/i18next-http-backend": { "node_modules/i18next-http-backend": {
"version": "3.0.2", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.5.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", "integrity": "sha512-QaWHnsxieEDcqKe+vo/RFqpiIFRi/KBqlOSPcUlvinBaISCeiTRCbtrazHAjtHtsLC66oDsROAH8frWkQzfMMQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"cross-fetch": "4.0.0" "cross-fetch": "4.1.0"
} }
}, },
"node_modules/i18next-resources-for-ts": { "node_modules/i18next-resources-for-ts": {
@@ -27108,9 +27110,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.5", "version": "0.2.7",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
"license": "MIT",
"engines": { "engines": {
"node": ">=14.14" "node": ">=14.14"
} }
+1 -1
View File
@@ -71,7 +71,7 @@
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "23.11.5", "i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "3.0.2", "i18next-http-backend": "3.0.5",
"i18next-resources-to-backend": "1.2.1", "i18next-resources-to-backend": "1.2.1",
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
"immer": "9.0.6", "immer": "9.0.6",
+3 -1
View File
@@ -1,3 +1,4 @@
import { stringifyError } from "Common/stringifyError";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import { isExpectedError } from "../Metrics/ErrorClassification"; import { isExpectedError } from "../Metrics/ErrorClassification";
@@ -20,6 +21,7 @@ export const handleError = (
consoleErrorPrefix?: string, consoleErrorPrefix?: string,
options?: HandleErrorOptions, options?: HandleErrorOptions,
): void => { ): void => {
console.log("{{cdbp}} in handleError(): raw error: " + stringifyError(error)); //CTODO in case a stray error happens
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
const errorCode = error instanceof ARMError ? error.code : undefined; const errorCode = error instanceof ARMError ? error.code : undefined;
@@ -44,7 +46,7 @@ export const handleError = (
export const getErrorMessage = (error: string | Error = ""): string => { export const getErrorMessage = (error: string | Error = ""): string => {
let errorMessage = typeof error === "string" ? error : error.message; let errorMessage = typeof error === "string" ? error : error.message;
if (!errorMessage) { if (!errorMessage) {
errorMessage = JSON.stringify(error); errorMessage = stringifyError(error);
} }
return replaceKnownError(errorMessage); return replaceKnownError(errorMessage);
}; };
+34 -17
View File
@@ -1,3 +1,4 @@
import { stringifyError } from "Common/stringifyError";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -26,6 +27,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
(userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo (userContext.fabricContext?.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]).resourceTokenInfo
.resourceTokens .resourceTokens
) { ) {
console.log("{{cdbp}} in readDatabases(): isFabricMirroredKey && has resourceTokens"); //CTODO should not get here
const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY]) const tokensData = (userContext.fabricContext.artifactInfo as FabricArtifactInfo[CosmosDbArtifactType.MIRRORED_KEY])
.resourceTokenInfo; .resourceTokenInfo;
@@ -59,6 +61,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
clearMessage(); clearMessage();
return databases; return databases;
} else if (isFabricNative() && userContext.fabricContext?.databaseName) { } else if (isFabricNative() && userContext.fabricContext?.databaseName) {
console.log("{{cdbp}} in readDatabases(): isFabricNative"); //CTODO should not get here
const databaseId = userContext.fabricContext.databaseName; const databaseId = userContext.fabricContext.databaseName;
databases = [ databases = [
{ {
@@ -81,9 +84,15 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
userContext.apiType !== "Tables" && userContext.apiType !== "Tables" &&
!isFabric() !isFabric()
) { ) {
console.log("{{cdbp}} in readDatabases(): authType == AAD, enableSDKOperations, apiType != Tables, !isFabric");
console.log("{{cdbp}} in readDatabases(): databaseaccount: " + userContext.databaseAccount);
console.log("{{cdbp}} in readDatabases(): calling readDatabasesWithARM");
databases = await readDatabasesWithARM(); databases = await readDatabasesWithARM();
console.log("{{cdbp}} in readDatabases(): done readDatabasesWithARM");
} else { } else {
console.log("{{cdbp}} in readDatabases(): calling SDK");
const sdkResponse = await client().databases.readAll().fetchAll(); const sdkResponse = await client().databases.readAll().fetchAll();
console.log("{{cdbp}} in readDatabases(): done SDK");
databases = sdkResponse.resources as DataModels.Database[]; databases = sdkResponse.resources as DataModels.Database[];
} }
} catch (error) { } catch (error) {
@@ -108,22 +117,30 @@ export async function readDatabasesWithARM(accountOverride?: {
const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? ""; const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? "";
const apiType = accountOverride?.apiType ?? userContext.apiType; const apiType = accountOverride?.apiType ?? userContext.apiType;
switch (apiType) { try {
case "SQL": switch (apiType) {
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName); case "SQL":
break; console.log("{{cdbp}} in readDatabasesWithARM(): calling listSqlDatabases");
case "Mongo": rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName); console.log("{{cdbp}} in readDatabasesWithARM(): done listSqlDatabases");
break; break;
case "Cassandra": case "Mongo":
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName); rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
break; break;
case "Gremlin": case "Cassandra":
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName); rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
break; break;
default: case "Gremlin":
throw new Error(`Unsupported default experience type: ${apiType}`); rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
} break;
default:
throw new Error(`Unsupported default experience type: ${apiType}`);
}
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? []; console.log("{{cdbp}} in readDatabasesWithARM(): response: " + JSON.stringify(rpResponse));
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
} catch (error) {
console.log("{{cdbp}} in readDatabasesWithARM(): ERROR: " + stringifyError(error));
throw error;
}
} }
+7
View File
@@ -0,0 +1,7 @@
export const stringifyError = (error: unknown): string => {
const plainObject: Record<string, unknown> = {};
Object.getOwnPropertyNames(error as object).forEach((key) => {
plainObject[key] = (error as Record<string, unknown>)[key];
});
return JSON.stringify(plainObject, null, "\r\n");
};
@@ -115,7 +115,7 @@ export const getCopyJobs = async (): Promise<CopyJobType[]> => {
}); });
return formattedJobs; return formattedJobs;
} catch (error) { } catch (error) {
const errorContent = JSON.stringify(error.content || error.message || error); const errorContent = String(error.content || error.message || error);
if (errorContent.includes("signal is aborted without reason")) { if (errorContent.includes("signal is aborted without reason")) {
throw { throw {
message: "Previous copy job request was cancelled.", message: "Previous copy job request was cancelled.",
+13 -1
View File
@@ -2,6 +2,7 @@ import * as msal from "@azure/msal-browser";
import { Link } from "@fluentui/react/lib/Link"; import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { stringifyError } from "Common/stringifyError";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
@@ -287,7 +288,7 @@ export default class Explorer {
"We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again", "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and try again",
); );
} else { } else {
const errorJson = JSON.stringify(error); const errorJson = stringifyError(error);
logConsoleError( logConsoleError(
`Failed to perform authorization for this account, due to the following error: \n${errorJson}`, `Failed to perform authorization for this account, due to the following error: \n${errorJson}`,
); );
@@ -401,19 +402,27 @@ export default class Explorer {
}, },
startKey, startKey,
); );
console.log("{{cdbp}} in refreshAllDatabases(): done readDatabases");
const currentDatabases = useDatabases.getState().databases; const currentDatabases = useDatabases.getState().databases;
console.log("{{cdbp}} in refreshAllDatabases(): currentDatabases: " + currentDatabases);
const deltaDatabases = this.getDeltaDatabases(databases, currentDatabases); const deltaDatabases = this.getDeltaDatabases(databases, currentDatabases);
console.log("{{cdbp}} in refreshAllDatabases(): deltaDatabases: " + deltaDatabases);
let updatedDatabases = currentDatabases.filter( let updatedDatabases = currentDatabases.filter(
(database) => !deltaDatabases.toDelete.some((deletedDatabase) => deletedDatabase.id() === database.id()), (database) => !deltaDatabases.toDelete.some((deletedDatabase) => deletedDatabase.id() === database.id()),
); );
console.log("{{cdbp}} in refreshAllDatabases(): updatedDatabases after filter: " + updatedDatabases);
updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) => updatedDatabases = [...updatedDatabases, ...deltaDatabases.toAdd].sort((db1, db2) =>
db1.id().localeCompare(db2.id()), db1.id().localeCompare(db2.id()),
); );
console.log("{{cdbp}} in refreshAllDatabases(): updatedDatabases after sort: " + updatedDatabases);
useDatabases.setState({ databases: updatedDatabases, databasesFetchedSuccessfully: true }); useDatabases.setState({ databases: updatedDatabases, databasesFetchedSuccessfully: true });
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched); scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
console.log("{{cdbp}} in refreshAllDatabases(): calling refreshAndExpandNewDatabases");
await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases); await this.refreshAndExpandNewDatabases(deltaDatabases.toAdd, updatedDatabases);
console.log("{{cdbp}} in refreshAllDatabases(): done refreshAndExpandNewDatabases");
} catch (error) { } catch (error) {
console.log("{{cdbp}} in refreshAllDatabases(): ERROR: " + stringifyError(error)); //CTODO this should be logged already but just in case
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.LoadDatabases, Action.LoadDatabases,
@@ -603,6 +612,7 @@ export default class Explorer {
? databases ? databases
: databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); : databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName);
console.log("{{cdbp}} in refreshAndExpandNewDatabases(): databasesToLoad: " + databasesToLoad);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
@@ -611,6 +621,7 @@ export default class Explorer {
try { try {
await Promise.all( await Promise.all(
databasesToLoad.map(async (database: ViewModels.Database) => { databasesToLoad.map(async (database: ViewModels.Database) => {
console.log("{{cdbp}} in refreshAndExpandNewDatabases(): loadCollections for database: " + database.id);
await database.loadCollections(true); await database.loadCollections(true);
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
if (isNewDatabase) { if (isNewDatabase) {
@@ -630,6 +641,7 @@ export default class Explorer {
// Start DatabaseTreeRendered — React render cycle will complete it in ResourceTree // Start DatabaseTreeRendered — React render cycle will complete it in ResourceTree
scenarioMonitor.startPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered); scenarioMonitor.startPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
} catch (error) { } catch (error) {
console.log("{{cdbp}} in refreshAndExpandNewDatabases(): ERROR: " + stringifyError(error)); //CTODO this should be logged already but just in case
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.LoadCollections, Action.LoadCollections,
{ {
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { stringifyError } from "Common/stringifyError";
import * as Q from "q"; import * as Q from "q";
import * as React from "react"; import * as React from "react";
import LoadGraphIcon from "../../../../images/LoadGraph.png"; import LoadGraphIcon from "../../../../images/LoadGraph.png";
@@ -1092,8 +1093,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) { public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
let errorDataStr = ""; let errorDataStr = "";
if (errorData && errorData.length > 0) { if (errorData && errorData.length > 0) {
console.error(msg, errorData); console.error(msg + String(errorData));
errorDataStr = ": " + JSON.stringify(errorData); errorDataStr = ": " + stringifyError(errorData);
} }
const consoleMessage = `${msg}${errorDataStr}`; const consoleMessage = `${msg}${errorDataStr}`;
@@ -1,6 +1,7 @@
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core"; import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
import { configuration } from "@nteract/mythic-configuration"; import { configuration } from "@nteract/mythic-configuration";
import { makeConfigureStore } from "@nteract/myths"; import { makeConfigureStore } from "@nteract/myths";
import { stringifyError } from "Common/stringifyError";
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux"; import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
import { Epic } from "redux-observable"; import { Epic } from "redux-observable";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
@@ -44,7 +45,7 @@ export default function configureStore(
const traceFailure = (title: string, error: any) => { const traceFailure = (title: string, error: any) => {
if (error instanceof Error) { if (error instanceof Error) {
onTraceFailure(title, `${error.message} ${JSON.stringify(error.stack)}`); onTraceFailure(title, `${error.message} ${stringifyError(error.stack)}`);
console.error(error); console.error(error);
} else { } else {
onTraceFailure(title, error.message); onTraceFailure(title, error.message);
@@ -1,3 +1,4 @@
import { stringifyError } from "Common/stringifyError";
import * as DataTables from "datatables.net"; import * as DataTables from "datatables.net";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
@@ -37,7 +38,7 @@ function parseError(err: any): ErrorDataModel[] {
try { try {
return _parse(err); return _parse(err);
} catch (e) { } catch (e) {
return [<ErrorDataModel>{ message: JSON.stringify(err) }]; return [<ErrorDataModel>{ message: stringifyError(err) }];
} }
} }
+6 -5
View File
@@ -1,4 +1,5 @@
import { FeedOptions } from "@azure/cosmos"; import { FeedOptions } from "@azure/cosmos";
import { stringifyError } from "Common/stringifyError";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q"; import Q from "q";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -172,7 +173,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(entity); deferred.resolve(entity);
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`); handleError(errorText, "AddRowCassandra", `Error while adding new row to table ${collection.id()}`);
deferred.reject(errorText); deferred.reject(errorText);
}, },
@@ -361,7 +362,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError( handleError(
errorText, errorText,
"CreateKeyspaceCassandra", "CreateKeyspaceCassandra",
@@ -400,7 +401,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(); deferred.resolve();
}, },
(error) => { (error) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError( handleError(
errorText, errorText,
"CreateTableCassandra", "CreateTableCassandra",
@@ -450,7 +451,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data); deferred.resolve(data);
}, },
(error: any) => { (error: any) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`); handleError(errorText, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
deferred.reject(errorText); deferred.reject(errorText);
}, },
@@ -492,7 +493,7 @@ export class CassandraAPIDataClient extends TableDataClient {
deferred.resolve(data.columns); deferred.resolve(data.columns);
}, },
(error: any) => { (error: any) => {
const errorText = error.responseJSON?.message ?? JSON.stringify(error); const errorText = error.responseJSON?.message ?? stringifyError(error);
handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`); handleError(errorText, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
deferred.reject(errorText); deferred.reject(errorText);
}, },
+3 -2
View File
@@ -1,5 +1,6 @@
import * as msal from "@azure/msal-browser"; import * as msal from "@azure/msal-browser";
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility"; import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
import { stringifyError } from "Common/stringifyError";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
@@ -154,9 +155,9 @@ export async function acquireMsalTokenForAccount(
traceFailure(Action.SignInAad, { traceFailure(Action.SignInAad, {
request: JSON.stringify(loginRequest), request: JSON.stringify(loginRequest),
acquireTokenType: silent ? "silent" : "interactive", acquireTokenType: silent ? "silent" : "interactive",
errorMessage: JSON.stringify(error), errorMessage: stringifyError(error),
}); });
traceFailure(Action.AcquireMsalToken, { error: JSON.stringify(error) }, msalStartKey); traceFailure(Action.AcquireMsalToken, { error: stringifyError(error) }, msalStartKey);
// Mark expected failure for health metrics so timeout emits healthy // Mark expected failure for health metrics so timeout emits healthy
if (isExpectedError(error)) { if (isExpectedError(error)) {
scenarioMonitor.markExpectedFailure(); scenarioMonitor.markExpectedFailure();
+136
View File
@@ -0,0 +1,136 @@
import { fetchWithTimeout, tryFetchWithTimeout } from "./FetchWithTimeout";
describe("fetchWithTimeout", () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
jest.useRealTimers();
});
it("forwards init and resolves with the fetch response", async () => {
const fakeResponse = { ok: true } as Response;
const fetchMock = jest.fn().mockResolvedValue(fakeResponse);
global.fetch = fetchMock as unknown as typeof global.fetch;
const result = await fetchWithTimeout("https://example.com", { method: "GET" }, 1000);
expect(result).toBe(fakeResponse);
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0];
expect(url).toBe("https://example.com");
expect((init as RequestInit).method).toBe("GET");
expect((init as RequestInit).signal).toBeDefined();
});
it("aborts the fetch when the internal timeout fires", async () => {
jest.useFakeTimers();
let receivedSignal: AbortSignal | undefined;
global.fetch = jest.fn().mockImplementation((_url: string, init: RequestInit) => {
receivedSignal = init.signal ?? undefined;
return new Promise<Response>((_resolve, reject) => {
init.signal?.addEventListener("abort", () => {
const err = new Error("aborted");
err.name = "AbortError";
reject(err);
});
});
}) as unknown as typeof global.fetch;
const pending = fetchWithTimeout("https://example.com", {}, 50);
jest.advanceTimersByTime(50);
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
expect(receivedSignal?.aborted).toBe(true);
});
it("throws immediately when the external signal is already aborted", async () => {
const fetchMock = jest.fn();
global.fetch = fetchMock as unknown as typeof global.fetch;
const controller = new AbortController();
const reason = new Error("user-cancelled");
controller.abort(reason);
await expect(fetchWithTimeout("https://example.com", { signal: controller.signal }, 1000)).rejects.toBe(reason);
expect(fetchMock).not.toHaveBeenCalled();
});
it("aborts mid-fetch when the external signal aborts and propagates the reason", async () => {
const reason = new Error("user-cancelled");
let internalSignal: AbortSignal | undefined;
global.fetch = jest.fn().mockImplementation((_url: string, init: RequestInit) => {
internalSignal = init.signal ?? undefined;
return new Promise<Response>((_resolve, reject) => {
init.signal?.addEventListener("abort", () => reject(init.signal?.reason ?? new Error("aborted")));
});
}) as unknown as typeof global.fetch;
const controller = new AbortController();
const pending = fetchWithTimeout("https://example.com", { signal: controller.signal }, 60000);
// Let the fetch wire up its abort listener before triggering.
await Promise.resolve();
controller.abort(reason);
await expect(pending).rejects.toBe(reason);
expect(internalSignal?.aborted).toBe(true);
});
it("skips the timer when timeoutMs is Infinity", async () => {
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
global.fetch = jest.fn().mockResolvedValue({ ok: true } as Response) as unknown as typeof global.fetch;
await fetchWithTimeout("https://example.com", {}, Infinity);
expect(setTimeoutSpy).not.toHaveBeenCalled();
setTimeoutSpy.mockRestore();
});
it("cleans up the timer and external listener after a successful fetch", async () => {
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
global.fetch = jest.fn().mockResolvedValue({ ok: true } as Response) as unknown as typeof global.fetch;
const controller = new AbortController();
const removeListenerSpy = jest.spyOn(controller.signal, "removeEventListener");
await fetchWithTimeout("https://example.com", { signal: controller.signal }, 1000);
expect(clearTimeoutSpy).toHaveBeenCalled();
expect(removeListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function));
clearTimeoutSpy.mockRestore();
});
it("cleans up the timer and listener when the fetch rejects", async () => {
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
const networkError = new Error("network down");
global.fetch = jest.fn().mockRejectedValue(networkError) as unknown as typeof global.fetch;
const controller = new AbortController();
const removeListenerSpy = jest.spyOn(controller.signal, "removeEventListener");
await expect(fetchWithTimeout("https://example.com", { signal: controller.signal }, 1000)).rejects.toBe(
networkError,
);
expect(clearTimeoutSpy).toHaveBeenCalled();
expect(removeListenerSpy).toHaveBeenCalledWith("abort", expect.any(Function));
clearTimeoutSpy.mockRestore();
});
});
describe("tryFetchWithTimeout", () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
});
it("returns null on fetch failure", async () => {
global.fetch = jest.fn().mockRejectedValue(new Error("boom")) as unknown as typeof global.fetch;
await expect(tryFetchWithTimeout("https://example.com")).resolves.toBeNull();
});
it("returns the response on success", async () => {
const response = { ok: true } as Response;
global.fetch = jest.fn().mockResolvedValue(response) as unknown as typeof global.fetch;
await expect(tryFetchWithTimeout("https://example.com")).resolves.toBe(response);
});
});
+26 -4
View File
@@ -3,6 +3,11 @@
* *
* Usage: await fetchWithTimeout(url, { method: 'GET', headers: {...} }, 10000); * Usage: await fetchWithTimeout(url, { method: 'GET', headers: {...} }, 10000);
* *
* If `init.signal` is provided, it is combined with the internal timeout: aborting
* the caller's signal aborts the fetch (propagating the caller's abort reason), and
* the timeout still applies. Pass `timeoutMs: Infinity` to disable the timeout entirely
* (useful for long-running operations that should rely solely on caller cancellation).
*
* A shared helper to remove duplicated inline implementations across the codebase. * A shared helper to remove duplicated inline implementations across the codebase.
*/ */
export async function fetchWithTimeout( export async function fetchWithTimeout(
@@ -10,13 +15,30 @@ export async function fetchWithTimeout(
init: RequestInit = {}, init: RequestInit = {},
timeoutMs: number = 5000, timeoutMs: number = 5000,
): Promise<Response> { ): Promise<Response> {
const externalSignal = init.signal;
if (externalSignal?.aborted) {
throw externalSignal.reason ?? new DOMException("The operation was aborted.", "AbortError");
}
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs); const hasTimeout = Number.isFinite(timeoutMs);
const timeoutId = hasTimeout ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
let onExternalAbort: (() => void) | undefined;
if (externalSignal) {
onExternalAbort = () => controller.abort(externalSignal.reason);
externalSignal.addEventListener("abort", onExternalAbort, { once: true });
}
try { try {
const response = await fetch(url, { ...init, signal: controller.signal }); return await fetch(url, { ...init, signal: controller.signal });
return response;
} finally { } finally {
clearTimeout(id); if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
if (externalSignal && onExternalAbort) {
externalSignal.removeEventListener("abort", onExternalAbort);
}
} }
} }
@@ -6,9 +6,10 @@
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/DocumentDB/preview/2025-11-01-preview/cosmos-db.json
*/ */
import { stringifyError } from "Common/stringifyError";
import { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; import * as Types from "./types";
import { configContext } from "../../../../ConfigContext";
const apiVersion = "2025-11-01-preview"; const apiVersion = "2025-11-01-preview";
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */ /* Lists the SQL databases under an existing Azure Cosmos DB database account. */
@@ -18,7 +19,14 @@ export async function listSqlDatabases(
accountName: string, accountName: string,
): Promise<Types.SqlDatabaseListResult> { ): Promise<Types.SqlDatabaseListResult> {
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases`; const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/sqlDatabases`;
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion }); console.log("{{cdbp}} in listSqlDatabases(): path: " + path);
try {
console.log("{{cdbp}} in listSqlDatabases(): calling armRequest");
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
} catch (error) {
console.log("{{cdbp}} in listSqlDatabases(): ERROR: " + stringifyError(error));
throw error;
}
} }
/* Gets the SQL database under an existing Azure Cosmos DB database account with the provided name. */ /* Gets the SQL database under an existing Azure Cosmos DB database account with the provided name. */
+196
View File
@@ -75,4 +75,200 @@ describe("ARM request", () => {
armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }), armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "GET" }),
).rejects.toThrow("No authority token provided"); ).rejects.toThrow("No authority token provided");
}); });
describe("timeout and retry behavior", () => {
beforeEach(() => {
updateUserContext({
authType: AuthType.AAD,
authorizationToken: "some-token",
});
});
const makeAbortError = () => Object.assign(new Error("aborted"), { name: "AbortError" });
const okResponse = () => ({
ok: true,
headers: new Headers(),
json: async () => ({}),
});
it("forwards timeoutMs to the underlying fetch timer", async () => {
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
window.fetch = jest.fn().mockResolvedValue(okResponse());
await armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "POST",
timeoutMs: 12345,
});
const timeoutValues = setTimeoutSpy.mock.calls.map((c) => c[1]);
expect(timeoutValues).toContain(12345);
setTimeoutSpy.mockRestore();
});
it("uses the default 5000ms timeout when timeoutMs is not provided", async () => {
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
window.fetch = jest.fn().mockResolvedValue(okResponse());
await armRequest({ apiVersion: "2001-01-01", host: "https://foo.com", path: "foo", method: "POST" });
const timeoutValues = setTimeoutSpy.mock.calls.map((c) => c[1]);
expect(timeoutValues).toContain(5000);
setTimeoutSpy.mockRestore();
});
it("skips the timer when timeoutMs is Infinity", async () => {
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
window.fetch = jest.fn().mockResolvedValue(okResponse());
await armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "POST",
timeoutMs: Infinity,
});
// No timer should be created by fetchWithTimeout.
expect(setTimeoutSpy).not.toHaveBeenCalled();
setTimeoutSpy.mockRestore();
});
it("retries GET on timeout with escalating timeouts and eventually succeeds", async () => {
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
const fetchMock = jest
.fn()
.mockRejectedValueOnce(makeAbortError())
.mockRejectedValueOnce(makeAbortError())
.mockResolvedValueOnce(okResponse());
window.fetch = fetchMock;
await armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "GET",
timeoutMs: 1000,
});
expect(fetchMock).toHaveBeenCalledTimes(3);
const timeoutValues = setTimeoutSpy.mock.calls.map((c) => c[1]);
// Each attempt creates a setTimeout for its escalating timeout (1x, 2x, 4x).
expect(timeoutValues).toContain(1000);
expect(timeoutValues).toContain(2000);
expect(timeoutValues).toContain(4000);
setTimeoutSpy.mockRestore();
});
it("gives up after exhausting retries and rejects with the last AbortError", async () => {
const fetchMock = jest.fn().mockRejectedValue(makeAbortError());
window.fetch = fetchMock;
await expect(
armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "GET",
timeoutMs: 1000,
}),
).rejects.toMatchObject({ name: "AbortError" });
// 3 attempts total (initial + 2 retries).
expect(fetchMock).toHaveBeenCalledTimes(3);
});
it("does not retry non-GET methods on timeout", async () => {
const fetchMock = jest.fn().mockRejectedValue(makeAbortError());
window.fetch = fetchMock;
for (const method of ["POST", "PUT", "PATCH", "DELETE", "HEAD"] as const) {
fetchMock.mockClear();
await expect(
armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method,
timeoutMs: 1000,
}),
).rejects.toBeTruthy();
expect(fetchMock).toHaveBeenCalledTimes(1);
}
});
it("does not retry GET on HTTP 500 (server error)", async () => {
const fetchMock = jest.fn().mockResolvedValue({
ok: false,
status: 500,
json: async () => ({ code: "InternalServerError", message: "boom" }),
});
window.fetch = fetchMock;
await expect(
armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "GET",
timeoutMs: 1000,
}),
).rejects.toThrow("boom");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("stops retrying GET when the caller's signal is already aborted", async () => {
const controller = new AbortController();
const reason = new Error("user-cancelled");
controller.abort(reason);
const fetchMock = jest.fn().mockRejectedValue(makeAbortError());
window.fetch = fetchMock;
await expect(
armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "GET",
timeoutMs: 1000,
signal: controller.signal,
}),
).rejects.toBeTruthy();
// fetchWithTimeout throws synchronously on already-aborted signal, so no fetch call.
expect(fetchMock).not.toHaveBeenCalled();
});
it("combines caller signal with timeout: aborting the signal cancels in-flight fetch", async () => {
const controller = new AbortController();
const reason = new Error("user-cancelled");
let receivedSignal: AbortSignal | undefined;
window.fetch = jest.fn().mockImplementation((_url: string, init: RequestInit) => {
receivedSignal = init.signal ?? undefined;
return new Promise<Response>((_resolve, reject) => {
init.signal?.addEventListener("abort", () => reject(init.signal?.reason ?? new Error("aborted")));
});
});
const pending = armRequest({
apiVersion: "2001-01-01",
host: "https://foo.com",
path: "foo",
method: "POST",
timeoutMs: 60000,
signal: controller.signal,
});
// Allow fetch to wire up its abort listener.
await Promise.resolve();
controller.abort(reason);
await expect(pending).rejects.toBe(reason);
expect(receivedSignal?.aborted).toBe(true);
});
});
}); });
+59 -2
View File
@@ -5,6 +5,7 @@ Instead, generate ARM clients that consume this function with stricter typing.
*/ */
import { stringifyError } from "Common/stringifyError";
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import { HttpHeaders } from "../../Common/Constants"; import { HttpHeaders } from "../../Common/Constants";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
@@ -50,8 +51,13 @@ interface Options {
contentType?: string; contentType?: string;
customHeaders?: Record<string, string>; customHeaders?: Record<string, string>;
signal?: AbortSignal; signal?: AbortSignal;
timeoutMs?: number;
} }
const DEFAULT_ARM_TIMEOUT_MS = 5000;
const isAbortError = (error: unknown): boolean => error instanceof Error && error.name === "AbortError";
export async function armRequestWithoutPolling<T>({ export async function armRequestWithoutPolling<T>({
host, host,
path, path,
@@ -62,6 +68,7 @@ export async function armRequestWithoutPolling<T>({
contentType, contentType,
customHeaders, customHeaders,
signal, signal,
timeoutMs,
}: Options): Promise<{ result: T; operationStatusUrl: string }> { }: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host); const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
@@ -71,6 +78,9 @@ export async function armRequestWithoutPolling<T>({
} }
if (!userContext?.authorizationToken && !customHeaders?.["Authorization"]) { if (!userContext?.authorizationToken && !customHeaders?.["Authorization"]) {
console.log(
"{{cdbp}} in armRequestWithoutPolling(): condition '!userContext?.authorizationToken && !customHeaders?.['Authorization']' met, throwing 'No authority token provided' error",
);
throw new Error("No authority token provided"); throw new Error("No authority token provided");
} }
@@ -84,10 +94,14 @@ export async function armRequestWithoutPolling<T>({
method, method,
headers, headers,
body: requestBody ? JSON.stringify(requestBody) : undefined, body: requestBody ? JSON.stringify(requestBody) : undefined,
...(signal ? { signal } : {}), signal,
}; };
const response = signal ? await window.fetch(url.href, fetchInit) : await fetchWithTimeout(url.href, fetchInit); const effectiveTimeoutMs = timeoutMs ?? DEFAULT_ARM_TIMEOUT_MS;
console.log(
`{{cdbp}} in armRequestWithoutPolling(): calling fetchWithRetry (method=${method}, timeoutMs=${effectiveTimeoutMs}, hasSignal=${!!signal})`,
);
const response = await fetchWithRetry(url.href, fetchInit, method, effectiveTimeoutMs, signal);
if (!response.ok) { if (!response.ok) {
let error: ARMError; let error: ARMError;
@@ -101,9 +115,11 @@ export async function armRequestWithoutPolling<T>({
error.code = errorResponse.code; error.code = errorResponse.code;
} }
} catch (error) { } catch (error) {
console.log("{{cdbp}} in armRequestWithoutPolling(): ERROR: " + stringifyError(error));
throw new Error(await response.text()); throw new Error(await response.text());
} }
console.log("{{cdbp}} in armRequestWithoutPolling(): ERROR: " + stringifyError(error));
throw error; throw error;
} }
@@ -123,6 +139,7 @@ export async function armRequest<T>({
contentType, contentType,
customHeaders, customHeaders,
signal, signal,
timeoutMs,
}: Options): Promise<T> { }: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({ const armRequestResult = await armRequestWithoutPolling<T>({
host, host,
@@ -134,6 +151,7 @@ export async function armRequest<T>({
contentType, contentType,
customHeaders, customHeaders,
signal, signal,
timeoutMs,
}); });
const operationStatusUrl = armRequestResult.operationStatusUrl; const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) { if (operationStatusUrl) {
@@ -142,6 +160,45 @@ export async function armRequest<T>({
return armRequestResult.result; return armRequestResult.result;
} }
/**
* Calls `fetchWithTimeout` once for non-idempotent methods. For idempotent GETs, retries
* up to {@link RETRY_TIMEOUT_MULTIPLIERS}.length attempts on timeout, escalating the timeout
* on each attempt. HTTP error responses (4xx/5xx) are NOT retried — they are surfaced by the
* caller's response.ok check. External `signal` cancellation aborts the retry loop immediately.
*/
async function fetchWithRetry(
url: string,
fetchInit: RequestInit,
method: Options["method"],
timeoutMs: number,
signal?: AbortSignal,
): Promise<Response> {
if (method !== "GET") {
return fetchWithTimeout(url, fetchInit, timeoutMs);
}
const RETRY_TIMEOUT_MULTIPLIERS = [1, 2, 4];
const RETRY_BACKOFF_MS = 100;
return promiseRetry(
(attemptNumber: number) => {
const attemptTimeoutMs =
timeoutMs * RETRY_TIMEOUT_MULTIPLIERS[Math.min(attemptNumber - 1, RETRY_TIMEOUT_MULTIPLIERS.length - 1)];
console.log(`{{cdbp}} in fetchWithRetry(): calling fetchWithTimeout: attempt=${attemptNumber} url=${url}`);
return fetchWithTimeout(url, fetchInit, attemptTimeoutMs);
},
{
retries: RETRY_TIMEOUT_MULTIPLIERS.length - 1,
factor: 1,
minTimeout: RETRY_BACKOFF_MS,
maxTimeout: RETRY_BACKOFF_MS,
signal,
// Only retry on timeout aborts. Caller cancellation (external signal aborted) stops retries.
shouldRetry: (error) => isAbortError(error) && !signal?.aborted,
},
);
}
async function getOperationStatus(operationStatusUrl: string) { async function getOperationStatus(operationStatusUrl: string) {
if (!userContext.authorizationToken) { if (!userContext.authorizationToken) {
throw new Error("No authority token provided"); throw new Error("No authority token provided");
+4 -3
View File
@@ -1,5 +1,6 @@
import * as msal from "@azure/msal-browser"; import * as msal from "@azure/msal-browser";
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import { stringifyError } from "Common/stringifyError";
import * as React from "react"; import * as React from "react";
import { ConfigContext } from "../ConfigContext"; import { ConfigContext } from "../ConfigContext";
import { import {
@@ -77,7 +78,7 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
localStorage.setItem("cachedTenantId", response.tenantId); localStorage.setItem("cachedTenantId", response.tenantId);
} catch (error) { } catch (error) {
setAuthFailure({ setAuthFailure({
failureMessage: `Login failed: ${JSON.stringify(error)}`, failureMessage: `Login failed: ${stringifyError(error)}`,
}); });
} }
}, [msalInstance, config]); }, [msalInstance, config]);
@@ -111,7 +112,7 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
localStorage.setItem("cachedTenantId", response.tenantId); localStorage.setItem("cachedTenantId", response.tenantId);
} catch (error) { } catch (error) {
setAuthFailure({ setAuthFailure({
failureMessage: `Tenant switch failed: ${JSON.stringify(error)}`, failureMessage: `Tenant switch failed: ${stringifyError(error)}`,
}); });
} }
}, },
@@ -144,7 +145,7 @@ export function useAADAuth(config?: ConfigContext): ReturnType {
failureLinkAction: acquireTokens, failureLinkAction: acquireTokens,
}); });
} else { } else {
const errorJson = JSON.stringify(error); const errorJson = stringifyError(error);
setAuthFailure({ setAuthFailure({
failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`, failureMessage: `We were unable to establish authorization for this account, due to the following error: \n${errorJson}`,
}); });