errors: add toError utility and migrate assertError usages

Add a `toError` utility function to `@backstage/errors` that converts
unknown values to `ErrorLike` objects. If the value is already error-like
it is returned as-is. Strings are used directly as the error message, and
other values are stringified with a fallback to JSON.stringify to avoid
unhelpful `[object Object]` messages.

Non-error causes passed to `CustomErrorBase` are now converted and stored
using `toError` rather than discarded. Existing `assertError` call sites
across the codebase are migrated to `toError`.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-03 01:33:34 +02:00
parent 3cdf048f77
commit b2319ffe45
70 changed files with 400 additions and 345 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/errors': minor
---
A new `toError` utility function is now available for converting unknown values to `ErrorLike` objects. If the value is already error-like it is returned as-is. Strings are used directly as the error message, and other values are stringified with a fallback to JSON to avoid unhelpful messages like `[object Object]`. Non-error causes passed to `CustomErrorBase` are now converted and stored using `toError` rather than discarded.
@@ -35,7 +35,7 @@ import type {
} from '../../../backend-plugin-api/src/wiring/types';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types';
import { ForwardedError, ConflictError, assertError } from '@backstage/errors';
import { ConflictError, ForwardedError, toError } from '@backstage/errors';
import { DependencyGraph } from '../lib/DependencyGraph';
import { ServiceRegistry } from './ServiceRegistry';
import { createInitializationResultCollector } from './createInitializationResultCollector';
@@ -385,12 +385,8 @@ export class BackendInitializer {
await moduleInit.init.func(moduleDeps);
resultCollector.onPluginModuleResult(pluginId, moduleId);
} catch (error: unknown) {
assertError(error);
resultCollector.onPluginModuleResult(
pluginId,
moduleId,
error,
);
const err = toError(error);
resultCollector.onPluginModuleResult(pluginId, moduleId, err);
}
},
);
@@ -414,8 +410,8 @@ export class BackendInitializer {
const lifecycleService = await this.#getPluginLifecycleImpl(pluginId);
await lifecycleService.startup();
} catch (error: unknown) {
assertError(error);
resultCollector.onPluginResult(pluginId, error);
const err = toError(error);
resultCollector.onPluginResult(pluginId, err);
}
}),
).catch(error => {
@@ -515,19 +511,19 @@ export class BackendInitializer {
throw new Error(`Invalid registration type '${(r as any).type}'`);
}
} catch (error: unknown) {
assertError(error);
const err = toError(error);
// Clean up partially registered extension points
for (const id of addedExtensionPointIds) {
this.#extensionPoints.delete(id);
}
if ('pluginId' in r && 'moduleId' in r) {
resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, error);
resultCollector.onPluginModuleResult(r.pluginId, r.moduleId, err);
} else if ('pluginId' in r) {
pluginInits.delete(r.pluginId);
moduleInits.delete(r.pluginId);
resultCollector.onPluginResult(r.pluginId, error);
resultCollector.onPluginResult(r.pluginId, err);
} else {
throw error;
throw err;
}
}
}
@@ -16,7 +16,7 @@
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import { decodeJwt, importJWK, SignJWT, decodeProtectedHeader } from 'jose';
import { assertError, AuthenticationError } from '@backstage/errors';
import { AuthenticationError, toError } from '@backstage/errors';
import { jwtVerify } from 'jose';
import { tokenTypes } from '@backstage/plugin-auth-node';
import { JwksClient } from '../JwksClient';
@@ -210,8 +210,10 @@ export class DefaultPluginTokenHandler implements PluginTokenHandler {
this.supportedTargetPlugins.add(targetPluginId);
return true;
} catch (error) {
assertError(error);
this.logger.error('Unexpected failure for target JWKS check', error);
this.logger.error(
'Unexpected failure for target JWKS check',
toError(error),
);
return false;
} finally {
this.targetPluginInflightChecks.delete(targetPluginId);
@@ -15,7 +15,7 @@
*/
import { LoggerService } from '@backstage/backend-plugin-api';
import { assertError } from '@backstage/errors';
import { isError, toError } from '@backstage/errors';
import { randomBytes } from 'node:crypto';
function handleBadError(error: Error, logger: LoggerService) {
@@ -37,11 +37,8 @@ export function applyInternalErrorFilter(
error: unknown,
logger: LoggerService,
): Error {
try {
assertError(error);
} catch (assertionError: unknown) {
assertError(assertionError);
return handleBadError(assertionError, logger);
if (!isError(error)) {
return handleBadError(toError(error), logger);
}
const constructorName = error.constructor.name;
@@ -32,11 +32,7 @@ import {
AwsCodeCommitIntegration,
ScmIntegrations,
} from '@backstage/integration';
import {
assertError,
ForwardedError,
NotModifiedError,
} from '@backstage/errors';
import { toError, ForwardedError, NotModifiedError } from '@backstage/errors';
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import {
CodeCommitClient,
@@ -418,8 +414,8 @@ export class AwsCodeCommitUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -33,11 +33,7 @@ import {
ScmIntegrations,
AwsS3IntegrationConfig,
} from '@backstage/integration';
import {
assertError,
ForwardedError,
NotModifiedError,
} from '@backstage/errors';
import { toError, ForwardedError, NotModifiedError } from '@backstage/errors';
import { fromTemporaryCredentials } from '@aws-sdk/credential-providers';
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
import {
@@ -400,8 +396,8 @@ export class AwsS3UrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -21,11 +21,7 @@ import {
StorageSharedKeyCredential,
} from '@azure/storage-blob';
import { ReaderFactory, ReadTreeResponseFactory } from './types';
import {
assertError,
ForwardedError,
NotModifiedError,
} from '@backstage/errors';
import { toError, ForwardedError, NotModifiedError } from '@backstage/errors';
import { Readable } from 'node:stream';
import { relative } from 'node:path/posix';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
@@ -261,9 +257,8 @@ export class AzureBlobStorageUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
throw error;
} catch (e) {
throw toError(e);
}
}
@@ -34,11 +34,7 @@ import {
} from '@backstage/integration';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import {
assertError,
NotFoundError,
NotModifiedError,
} from '@backstage/errors';
import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
@@ -212,8 +208,8 @@ export class AzureUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -23,11 +23,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
import {
assertError,
NotFoundError,
NotModifiedError,
} from '@backstage/errors';
import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import {
BitbucketCloudIntegration,
getBitbucketCloudDefaultBranch,
@@ -196,8 +192,8 @@ export class BitbucketCloudUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -23,11 +23,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
import {
assertError,
NotFoundError,
NotModifiedError,
} from '@backstage/errors';
import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import {
BitbucketServerIntegration,
getBitbucketServerDownloadUrl,
@@ -180,8 +176,8 @@ export class BitbucketServerUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -22,11 +22,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
import {
assertError,
NotFoundError,
NotModifiedError,
} from '@backstage/errors';
import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReaderFactory } from './types';
import path from 'node:path';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
@@ -236,8 +232,8 @@ export class FetchUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -40,7 +40,7 @@ import {
NotFoundError,
NotModifiedError,
ResponseError,
assertError,
toError,
} from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
import { Minimatch } from 'minimatch';
@@ -174,8 +174,8 @@ export class GerritUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -35,7 +35,7 @@ import {
import { ReaderFactory, ReadTreeResponseFactory } from './types';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import {
assertError,
toError,
AuthenticationError,
NotFoundError,
NotModifiedError,
@@ -189,8 +189,8 @@ export class GiteaUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -37,11 +37,7 @@ import fetch, { RequestInit, Response } from 'node-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'node:stream';
import {
assertError,
NotFoundError,
NotModifiedError,
} from '@backstage/errors';
import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory, ReaderFactory } from './types';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import { parseLastModified } from './util';
@@ -209,8 +205,8 @@ export class GithubUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -23,11 +23,7 @@ import {
UrlReaderServiceSearchOptions,
UrlReaderServiceSearchResponse,
} from '@backstage/backend-plugin-api';
import {
assertError,
NotFoundError,
NotModifiedError,
} from '@backstage/errors';
import { toError, NotFoundError, NotModifiedError } from '@backstage/errors';
import {
getGitLabFileFetchUrl,
getGitLabIntegrationRelativePath,
@@ -260,8 +256,8 @@ export class GitlabUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -34,7 +34,7 @@ import {
import { Readable } from 'node:stream';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import packageinfo from '../../../../package.json';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { relative } from 'node:path/posix';
const GOOGLE_GCS_HOST = 'storage.cloud.google.com';
@@ -182,8 +182,8 @@ export class GoogleGcsUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
@@ -36,7 +36,7 @@ import { ReadTreeResponseFactory, ReaderFactory } from './types';
import fetch, { Response } from 'node-fetch';
import { ReadUrlResponseFactory } from './ReadUrlResponseFactory';
import {
assertError,
toError,
AuthenticationError,
NotFoundError,
NotModifiedError,
@@ -187,8 +187,8 @@ export class HarnessUrlReader implements UrlReaderService {
],
etag: data.etag ?? '',
};
} catch (error) {
assertError(error);
} catch (e) {
const error = toError(e);
if (error.name === 'NotFoundError') {
return {
files: [],
+5 -5
View File
@@ -17,7 +17,7 @@
import { ChildProcess, SpawnOptions } from 'node:child_process';
import spawn from 'cross-spawn';
import { ExitCodeError } from './errors';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
/**
* Callback function that can be used to receive stdout or stderr data from a child process.
@@ -193,14 +193,14 @@ export async function runOutput(
return Buffer.concat(stdoutChunks).toString().trim();
} catch (error) {
assertError(error);
const err = toError(error);
(error as Error & { stdout?: string }).stdout =
(err as Error & { stdout?: string }).stdout =
Buffer.concat(stdoutChunks).toString();
(error as Error & { stderr?: string }).stderr =
(err as Error & { stderr?: string }).stderr =
Buffer.concat(stderrChunks).toString();
throw error;
throw err;
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError, ForwardedError } from '@backstage/errors';
import { ForwardedError } from '@backstage/errors';
import { runOutput } from '@backstage/cli-common';
const versions = new Map<string, Promise<'classic' | 'berry'>>();
@@ -32,7 +32,6 @@ export function detectYarnVersion(dir?: string): Promise<'classic' | 'berry'> {
});
return stdout.trim().startsWith('1.') ? 'classic' : 'berry';
} catch (error) {
assertError(error);
throw new ForwardedError('Failed to determine yarn version', error);
}
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { addCodeownersEntry } from '../codeowners';
import { Task } from '../tasks';
import {
@@ -68,9 +68,9 @@ export async function executePortableTemplate(
}).waitForExit();
});
} catch (error) {
assertError(error);
const err = toError(error);
Task.error(
`Warning: Failed to execute command '${commandStr}', ${error}`,
`Warning: Failed to execute command '${commandStr}', ${err}`,
);
}
}
@@ -80,8 +80,8 @@ export async function executePortableTemplate(
Task.log(`🎉 Successfully created ${template.name}`);
Task.log();
} catch (error) {
assertError(error);
Task.error(error.message);
const err = toError(error);
Task.error(err.message);
if (modified) {
Task.log('It seems that something went wrong in the creation process 🤔');
+5 -8
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError, ForwardedError } from '@backstage/errors';
import { ForwardedError, toError } from '@backstage/errors';
import { targetPaths } from '@backstage/cli-common';
import { runOutput } from '@backstage/cli-common';
@@ -28,13 +28,10 @@ export async function runGit(...args: string[]) {
});
return stdout.trim().split(/\r\n|\r|\n/);
} catch (error) {
assertError(error);
if (
'code' in error &&
typeof (error as { code?: number }).code === 'number'
) {
const code = (error as { code?: number }).code;
const stderr = (error as { stderr?: string }).stderr;
const err = toError(error);
if ('code' in err && typeof (err as { code?: number }).code === 'number') {
const code = (err as { code?: number }).code;
const stderr = (err as { stderr?: string }).stderr;
const msg = stderr?.trim() ?? `with exit code ${code}`;
throw new Error(`git ${args[0]} failed, ${msg}`);
}
+1 -6
View File
@@ -14,11 +14,7 @@
* limitations under the License.
*/
import {
assertError,
ForwardedError,
NotImplementedError,
} from '@backstage/errors';
import { ForwardedError, NotImplementedError } from '@backstage/errors';
import { PackageInfo, PackageManager } from '../PackageManager';
import { Lockfile } from '../Lockfile';
import { YarnVersion } from './types';
@@ -96,7 +92,6 @@ function detectYarnVersion(dir?: string): Promise<YarnVersion> {
: 'berry';
return { version: versionString, codename };
} catch (error) {
assertError(error);
throw new ForwardedError('Failed to determine yarn version', error);
}
});
+2 -3
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { exitWithError } from './errors';
type ActionFunc = (...args: any[]) => Promise<void>;
@@ -40,8 +40,7 @@ export function lazy<TModule extends object>(
process.exit(0);
} catch (error) {
assertError(error);
exitWithError(error);
exitWithError(toError(error));
}
};
}
+4 -4
View File
@@ -24,7 +24,7 @@ import {
} from 'node:path';
import { ConfigSchemaPackageEntry } from './types';
import { JsonObject } from '@backstage/types';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
type Item = {
name?: string;
@@ -249,9 +249,9 @@ async function compileTsSchemas(
);
}
} catch (error) {
assertError(error);
if (error.message !== 'type Config not found') {
throw error;
const err = toError(error);
if (err.message !== 'type Config not found') {
throw err;
}
}
@@ -15,7 +15,7 @@
*/
import { AppConfig } from '@backstage/config';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { JsonObject } from '@backstage/types';
import { AsyncConfigSourceGenerator, ConfigSource } from './types';
@@ -165,7 +165,6 @@ function safeJsonParse(str: string): [Error | null, any] {
try {
return [null, JSON.parse(str)];
} catch (err) {
assertError(err);
return [err, str];
return [toError(err), str];
}
}
@@ -15,7 +15,7 @@
*/
import { JsonObject, JsonValue } from '@backstage/types';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { TransformContext, TransformFunc } from './types';
import { isObject } from './utils';
import { createSubstitutionTransform } from './substitution';
@@ -50,8 +50,7 @@ export async function applyConfigTransforms(
break;
}
} catch (error) {
assertError(error);
throw new Error(`error at ${path}, ${error.message}`);
throw new Error(`error at ${path}, ${toError(error).message}`);
}
}
@@ -21,7 +21,7 @@ import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import { createElement, isValidElement, useState } from 'react';
import { isError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import {
configApiRef,
IconComponent,
@@ -63,7 +63,7 @@ const LoginRequestListItem = ({ request, busy, setBusy }: RowProps) => {
try {
await request.trigger();
} catch (e) {
setError(isError(e) ? e.message : 'An unspecified error occurred');
setError(toError(e).message);
} finally {
setBusy(false);
}
+3
View File
@@ -155,4 +155,7 @@ export class ServiceUnavailableError extends CustomErrorBase {}
// @public
export function stringifyError(error: unknown): string;
// @public
export function toError(value: unknown): ErrorLike;
```
@@ -15,7 +15,7 @@
*/
import { stringifyError } from '../serialization/error';
import { isError } from './assertion';
import { toError } from './assertion';
/**
* A base class that custom Error classes can inherit from.
@@ -63,6 +63,6 @@ export class CustomErrorBase extends Error {
}
}
this.cause = isError(cause) ? cause : undefined;
this.cause = cause !== undefined ? toError(cause) : undefined;
}
}
+83 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError, isError } from './assertion';
import { assertError, isError, toError } from './assertion';
import { NotFoundError } from './common';
import { CustomErrorBase } from './CustomErrorBase';
@@ -77,3 +77,85 @@ describe('isError', () => {
},
);
});
describe('toError', () => {
it.each(areErrors)(
'should pass through error-like values as-is %#',
error => {
expect(toError(error)).toBe(error);
},
);
it('should preserve the original error instance', () => {
const original = new NotFoundError('not found');
expect(toError(original)).toBe(original);
});
it('should use strings directly as the error message', () => {
const result = toError('something went wrong');
expect(result).toBeInstanceOf(Error);
expect(result.message).toBe('something went wrong');
});
it('should handle empty strings', () => {
const result = toError('');
expect(result).toBeInstanceOf(Error);
expect(result.message).toBe('');
});
it('should wrap undefined', () => {
const result = toError(undefined);
expect(result).toBeInstanceOf(Error);
expect(result.message).toBe("unknown error 'undefined'");
});
it('should wrap null', () => {
const result = toError(null);
expect(result).toBeInstanceOf(Error);
expect(result.message).toBe("unknown error 'null'");
});
it('should wrap numbers', () => {
expect(toError(0).message).toBe("unknown error '0'");
expect(toError(42).message).toBe("unknown error '42'");
});
it('should wrap booleans', () => {
expect(toError(false).message).toBe("unknown error 'false'");
expect(toError(true).message).toBe("unknown error 'true'");
});
it('should wrap plain objects using JSON when toString is unhelpful', () => {
expect(toError({ name: 'e' }).message).toBe(`unknown error '{"name":"e"}'`);
expect(toError({ message: '' }).message).toBe(
`unknown error '{"message":""}'`,
);
expect(toError({ code: 404, detail: 'missing' }).message).toBe(
`unknown error '{"code":404,"detail":"missing"}'`,
);
});
it('should fall back to [object Object] for empty plain objects', () => {
expect(toError({}).message).toBe("unknown error '[object Object]'");
});
it('should wrap arrays', () => {
expect(toError([]).message).toBe("unknown error ''");
expect(toError([1, 2]).message).toBe("unknown error '1,2'");
});
it('should handle objects with a custom toString', () => {
const obj = { toString: () => 'custom string' };
expect(toError(obj).message).toBe("unknown error 'custom string'");
});
it('should handle symbols', () => {
const result = toError(Symbol('test'));
expect(result).toBeInstanceOf(Error);
expect(result.message).toBe("unknown error 'Symbol(test)'");
});
it('should handle BigInt', () => {
expect(toError(BigInt(42)).message).toBe("unknown error '42'");
});
});
+27
View File
@@ -71,3 +71,30 @@ export function assertError(value: unknown): asserts value is ErrorLike {
);
}
}
/**
* Converts an unknown value to an {@link ErrorLike} object.
*
* If the value is already an {@link ErrorLike} object, it is returned as-is. Otherwise, a new
* `Error` is created with the value stringified as the message.
*
* @public
* @param value - an unknown value
* @returns an {@link ErrorLike} object
*/
export function toError(value: unknown): ErrorLike {
if (isError(value)) {
return value;
}
if (typeof value === 'string') {
return new Error(value) as ErrorLike;
}
const str = String(value);
if (str === '[object Object]') {
const json = JSON.stringify(value);
if (json !== '{}') {
return new Error(`unknown error '${json}'`) as ErrorLike;
}
}
return new Error(`unknown error '${str}'`) as ErrorLike;
}
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
export { assertError, isError } from './assertion';
export { assertError, isError, toError } from './assertion';
export type { ErrorLike } from './assertion';
export {
AuthenticationError,
+2 -3
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { Command } from 'commander';
import { exitWithError } from '../lib/errors';
@@ -302,8 +302,7 @@ export function lazy<TModule extends object>(
process.exit(0);
} catch (error) {
assertError(error);
exitWithError(error);
exitWithError(toError(error));
}
};
}
@@ -23,6 +23,7 @@ import {
WriteResult,
} from '@google-cloud/firestore';
import { toError } from '@backstage/errors';
import { AnyJWK, KeyStore, StoredKey } from './types';
export type FirestoreKeyStoreSettings = Settings & Options;
@@ -66,14 +67,11 @@ export class FirestoreKeyStore implements KeyStore {
try {
await keyStore.verify();
} catch (error) {
const err = toError(error);
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to connect to database: ${(error as Error).message}`,
);
throw new Error(`Failed to connect to database: ${err.message}`);
}
logger?.warn(
`Failed to connect to database: ${(error as Error).message}`,
);
logger?.warn(`Failed to connect to database: ${err.message}`);
}
}
+7 -4
View File
@@ -16,7 +16,7 @@
import { AuthService, LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { assertError, NotFoundError } from '@backstage/errors';
import { NotFoundError, toError } from '@backstage/errors';
import {
AuthOwnershipResolver,
AuthProviderFactory,
@@ -104,14 +104,17 @@ export function bindProviderRouters(
targetRouter.use(`/${providerId}`, r);
} catch (e) {
assertError(e);
if (process.env.NODE_ENV !== 'development') {
throw new Error(
`Failed to initialize ${providerId} auth provider, ${e.message}`,
`Failed to initialize ${providerId} auth provider, ${
toError(e).message
}`,
);
}
logger.warn(`Skipping ${providerId} auth provider, ${e.message}`);
logger.warn(
`Skipping ${providerId} auth provider, ${toError(e).message}`,
);
targetRouter.use(`/${providerId}`, () => {
// If the user added the provider under auth.providers but the clientId and clientSecret etc. were not found.
@@ -23,7 +23,7 @@ import {
import { useCallback } from 'react';
import useAsync from 'react-use/esm/useAsync';
import useAsyncFn from 'react-use/esm/useAsyncFn';
import { isError } from '@backstage/errors';
import { toError } from '@backstage/errors';
interface Session {
id: string;
@@ -111,9 +111,7 @@ export const useConsentSession = (opts: { sessionId?: string }) => {
if (sessionState.error) {
return {
status: 'error',
error: isError(sessionState.error)
? sessionState.error.message
: 'Failed to load consent request',
error: toError(sessionState.error).message,
};
}
if (sessionState.value) {
@@ -133,7 +131,7 @@ export const useConsentSession = (opts: { sessionId?: string }) => {
await handleActionInternal(action, state.session);
} catch (err) {
alertApi.post({
message: isError(err) ? err.message : `Failed to ${action} consent`,
message: toError(err).message,
severity: 'error',
});
}
@@ -21,7 +21,7 @@ import { IncrementalIngestionDatabaseManager } from '../database/IncrementalInge
import { performance } from 'node:perf_hooks';
import { Duration } from 'luxon';
import { v4 } from 'uuid';
import { stringifyError } from '@backstage/errors';
import { stringifyError, toError } from '@backstage/errors';
import { EventParams } from '@backstage/plugin-events-node';
import { HumanDuration } from '@backstage/types';
@@ -123,17 +123,12 @@ export class IncrementalIngestionEngine implements IterationEngine {
);
}
} catch (error) {
if (
(error as Error).message &&
(error as Error).message === 'CANCEL'
) {
const err = toError(error);
if (err.message === 'CANCEL') {
this.options.logger.info(
`incremental-engine: Ingestion '${ingestionId}' canceled`,
);
await this.manager.setProviderCanceling(
ingestionId,
(error as Error).message,
);
await this.manager.setProviderCanceling(ingestionId, err.message);
} else {
const currentBackoff = Duration.fromObject(
this.backoff[Math.min(this.backoff.length - 1, attempts)],
@@ -19,7 +19,7 @@ import {
Entity,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { assertError, serializeError, stringifyError } from '@backstage/errors';
import { serializeError, stringifyError, toError } from '@backstage/errors';
import { Hash } from 'node:crypto';
import stableStringify from 'fast-json-stable-stringify';
import { Knex } from 'knex';
@@ -344,8 +344,7 @@ export class DefaultCatalogProcessingEngine {
track.markSuccessfulWithChanges();
} catch (error) {
assertError(error);
track.markFailed(error);
track.markFailed(toError(error));
}
});
},
@@ -24,10 +24,10 @@ import {
stringifyLocationRef,
} from '@backstage/catalog-model';
import {
assertError,
ConflictError,
InputError,
NotAllowedError,
toError,
} from '@backstage/errors';
import { JsonValue } from '@backstage/types';
import { ScmIntegrationRegistry } from '@backstage/integration';
@@ -192,10 +192,10 @@ export class DefaultCatalogProcessingOrchestrator
ok: collectorResults.errors.length === 0,
};
} catch (error) {
assertError(error);
const err = toError(error);
return {
ok: false,
errors: collector.results().errors.concat(error),
errors: collector.results().errors.concat(err),
};
}
}
@@ -21,7 +21,7 @@ import {
stringifyEntityRef,
stringifyLocationRef,
} from '@backstage/catalog-model';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import {
CatalogProcessor,
CatalogProcessorResult,
@@ -97,9 +97,11 @@ export class ProcessorOutputCollector {
try {
entity = validateEntityEnvelope(i.entity);
} catch (e) {
assertError(e);
logger.debug(`Envelope validation failed at ${location}, ${e}`);
this.errors.push(e);
const validationError = toError(e);
logger.debug(
`Envelope validation failed at ${location}, ${validationError}`,
);
this.errors.push(validationError);
return;
}
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import limiterFactory, { Limit } from 'p-limit';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
@@ -109,18 +109,18 @@ export class UrlReaderProcessor implements CatalogProcessor {
emit(processingResult.refresh(`${location.type}:${location.target}`));
} catch (error) {
assertError(error);
const message = `Unable to read ${location.type}, ${error}`.substring(
const err = toError(error);
const message = `Unable to read ${location.type}, ${err}`.substring(
0,
5000,
);
if (error.name === 'NotModifiedError' && cacheItem) {
if (err.name === 'NotModifiedError' && cacheItem) {
for (const parseResult of cacheItem.value) {
emit(parseResult);
}
emit(processingResult.refresh(`${location.type}:${location.target}`));
await cache.set(CACHE_KEY, cacheItem);
} else if (error.name === 'NotFoundError') {
} else if (err.name === 'NotFoundError') {
if (!optional) {
emit(processingResult.notFoundError(location, message));
}
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import {
catalogApiRef,
@@ -210,8 +210,7 @@ export const StepPrepareCreatePullRequest = (
{ notRepeatable: true },
);
} catch (e) {
assertError(e);
setError(e.message);
setError(toError(e).message);
setSubmitted(false);
}
},
@@ -17,7 +17,7 @@
import { stringifyEntityRef } from '@backstage/catalog-model';
import { Link } from '@backstage/core-components';
import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import FormHelperText from '@material-ui/core/FormHelperText';
@@ -94,12 +94,12 @@ export const StepReviewLocation = ({
locations,
});
} catch (e) {
assertError(e);
const caughtError = toError(e);
// TODO: this error should be handled differently. We add it as 'optional' and
// it is not uncommon that a PR has not been merged yet.
if (
prepareResult.type === 'repository' &&
e.message.startsWith(
caughtError.message.startsWith(
'Location was added but has no entities specified yet',
)
) {
@@ -111,7 +111,7 @@ export const StepReviewLocation = ({
})),
});
} else {
setError(e.message);
setError(caughtError.message);
setSubmitted(false);
}
}
@@ -25,7 +25,7 @@ import {
import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api';
import { Progress, ResponseErrorPanel } from '@backstage/core-components';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { catalogReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import {
@@ -75,8 +75,7 @@ function useUnregisterDialogHandlers(
await state.unregisterLocation();
onConfirm();
} catch (err) {
assertError(err);
alertApi.post({ message: err.message });
alertApi.post({ message: toError(err).message });
} finally {
setBusyAction(null);
}
@@ -98,8 +97,7 @@ function useUnregisterDialogHandlers(
display: 'transient',
});
} catch (err) {
assertError(err);
alertApi.post({ message: err.message });
alertApi.post({ message: toError(err).message });
} finally {
setBusyAction(null);
}
@@ -19,7 +19,7 @@ import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogTitle from '@material-ui/core/DialogTitle';
import { useState } from 'react';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
interface DeleteEntityConfirmationProps {
open: boolean;
@@ -37,7 +37,7 @@ export function DeleteEntityConfirmationDialog(
try {
onConfirm();
} catch (err) {
assertError(err);
void toError(err);
} finally {
setBusy(false);
}
@@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { useState } from 'react';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { catalogTranslationRef } from '../../alpha/translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { Button, Dialog, DialogFooter, DialogHeader } from '@backstage/ui';
@@ -44,8 +44,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) {
await catalogApi.removeEntityByUid(uid!);
onConfirm();
} catch (err) {
assertError(err);
alertApi.post({ message: err.message });
alertApi.post({ message: toError(err).message });
} finally {
setBusy(false);
}
@@ -32,7 +32,7 @@ import os from 'node:os';
import fs from 'fs-extra';
import { Lockfile } from '../util/Lockfile';
import { memoize } from 'lodash';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { LoggerService } from '@backstage/backend-plugin-api';
/** @public */
@@ -179,7 +179,7 @@ export class DevToolsBackendApi {
const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
configInfo.config = data;
} catch (error) {
assertError(error);
const err = toError(error);
// The config is not valid for some reason but we want to be able to see it still
const config = {
data: this.config.get() as JsonObject,
@@ -194,10 +194,10 @@ export class DevToolsBackendApi {
const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
configInfo.config = data;
configInfo.error = {
name: error.name,
message: error.message,
messages: error.messages as string[] | undefined,
stack: error.stack,
name: err.name,
message: err.message,
messages: err.messages as string[] | undefined,
stack: err.stack,
};
}
@@ -20,7 +20,7 @@ import { McpService } from '../services/McpService';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
import { HttpAuthService, LoggerService } from '@backstage/backend-plugin-api';
import { isError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { MetricsService } from '@backstage/backend-plugin-api/alpha';
import { bucketBoundaries, McpServerSessionAttributes } from '../metrics';
import { McpServerConfig } from '../config';
@@ -83,11 +83,10 @@ export const createStreamableRouter = ({
sessionDuration.record(durationSeconds, baseAttributes);
});
} catch (error) {
const errorType = isError(error) ? error.name : 'Error';
const err = toError(error);
const errorType = err.name;
if (isError(error)) {
logger.error(error.message);
}
logger.error(err.message);
if (!res.headersSent) {
res.status(500).json({
@@ -27,7 +27,7 @@ import {
UserEntity,
} from '@backstage/catalog-model';
import { Config, readDurationFromConfig } from '@backstage/config';
import { NotFoundError } from '@backstage/errors';
import { NotFoundError, toError } from '@backstage/errors';
import { Notification } from '@backstage/plugin-notifications-common';
import {
NotificationProcessor,
@@ -238,9 +238,7 @@ export class SlackNotificationProcessor implements NotificationProcessor {
channel = await this.getSlackNotificationTarget(entityRef);
} catch (error) {
this.logger.error(
`Failed to get Slack channel for entity: ${
(error as Error).message
}`,
`Failed to get Slack channel for entity: ${toError(error).message}`,
);
return;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { LoggerService } from '@backstage/backend-plugin-api';
@@ -109,9 +109,9 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({
});
}
} catch (e) {
assertError(e);
const error = toError(e);
if (
e.message.includes(
error.message.includes(
'Upgrade to GitHub Pro or make this repository public to enable this feature',
)
) {
@@ -119,7 +119,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({
'Branch protection was not enabled as it requires GitHub Pro for private repositories',
);
} else {
throw e;
throw error;
}
}
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError, InputError } from '@backstage/errors';
import { InputError, toError } from '@backstage/errors';
import {
GithubCredentialsProvider,
ScmIntegrations,
@@ -175,11 +175,11 @@ export function createGithubActionsDispatchAction(options: {
}
}
} catch (e) {
assertError(e);
const error = toError(e);
ctx.logger.warn(
`Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${e.message}`,
`Failed: dispatching workflow '${workflowId}' on repo: '${repo}', ${error.message}`,
);
throw e;
throw error;
}
},
});
@@ -22,7 +22,7 @@ import {
createTemplateAction,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { assertError, InputError } from '@backstage/errors';
import { InputError, toError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { getOctokitOptions } from '../util';
import { examples } from './githubIssuesCreate.examples';
@@ -171,11 +171,11 @@ export function createGithubIssuesCreateAction(options: {
`Successfully created issue #${issue.number}: ${issue.html_url}`,
);
} catch (e) {
assertError(e);
const error = toError(e);
ctx.logger.warn(
`Failed: creating issue '${title}' on repo: '${repo}', ${e.message}`,
`Failed: creating issue '${title}' on repo: '${repo}', ${error.message}`,
);
throw e;
throw error;
}
},
});
@@ -22,7 +22,7 @@ import {
createTemplateAction,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { assertError, InputError } from '@backstage/errors';
import { InputError, toError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { getOctokitOptions } from '../util';
import { examples } from './githubIssuesLabel.examples';
@@ -101,9 +101,10 @@ export function createGithubIssuesLabelAction(options: {
},
});
} catch (e) {
assertError(e);
ctx.logger.warn(
`Failed: adding labels to issue: '${number}' on repo: '${repo}', ${e.message}`,
`Failed: adding labels to issue: '${number}' on repo: '${repo}', ${
toError(e).message
}`,
);
}
},
@@ -23,7 +23,7 @@ import {
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { emitterEventNames } from '@octokit/webhooks';
import { assertError, InputError } from '@backstage/errors';
import { InputError, toError } from '@backstage/errors';
import { Octokit } from 'octokit';
import { getOctokitOptions } from '../util';
import { examples } from './githubWebhook.examples';
@@ -173,9 +173,10 @@ export function createGithubWebhookAction(options: {
ctx.logger.info(`Webhook '${webhookUrl}' created successfully`);
} catch (e) {
assertError(e);
ctx.logger.warn(
`Failed: create webhook '${webhookUrl}' on repo: '${repo}', ${e.message}`,
`Failed: create webhook '${webhookUrl}' on repo: '${repo}', ${
toError(e).message
}`,
);
}
},
@@ -15,7 +15,7 @@
*/
import { Config } from '@backstage/config';
import { assertError, NotFoundError } from '@backstage/errors';
import { NotFoundError, toError } from '@backstage/errors';
import { Octokit } from 'octokit';
import {
@@ -124,14 +124,15 @@ export async function createGithubRepoWithCollaboratorsAndTopics(
try {
newRepo = (await repoCreationPromise).data;
} catch (e) {
assertError(e);
if (e.message === 'Resource not accessible by integration') {
if (toError(e).message === 'Resource not accessible by integration') {
logger.warn(
`The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`,
);
}
throw new Error(
`Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`,
`Failed to create the ${user.data.type} repository ${owner}/${repo}, ${
toError(e).message
}`,
);
}
@@ -174,10 +175,11 @@ export async function createGithubRepoWithCollaboratorsAndTopics(
});
}
} catch (e) {
assertError(e);
const name = extractCollaboratorName(collaborator);
logger.warn(
`Skipping ${collaborator.access} access for ${name}, ${e.message}`,
`Skipping ${collaborator.access} access for ${name}, ${
toError(e).message
}`,
);
}
}
@@ -191,8 +193,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics(
names: topics.map(t => t.toLowerCase()),
});
} catch (e) {
assertError(e);
logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`);
logger.warn(`Skipping topics ${topics.join(' ')}, ${toError(e).message}`);
}
}
@@ -356,9 +357,10 @@ export async function initRepoPushAndProtect(
requiredLinearHistory,
});
} catch (e) {
assertError(e);
logger.warn(
`Skipping: default branch protection on '${repo}', ${e.message}`,
`Skipping: default branch protection on '${repo}', ${
toError(e).message
}`,
);
}
}
@@ -369,8 +371,12 @@ export async function initRepoPushAndProtect(
function extractCollaboratorName(
collaborator: { user: string } | { team: string } | { username: string },
) {
if ('username' in collaborator) return collaborator.username;
if ('user' in collaborator) return collaborator.user;
if ('username' in collaborator) {
return collaborator.username;
}
if ('user' in collaborator) {
return collaborator.user;
}
return collaborator.team;
}
@@ -16,7 +16,7 @@
import { AuditorService, LoggerService } from '@backstage/backend-plugin-api';
import type { MetricsService } from '@backstage/backend-plugin-api/alpha';
import { assertError, InputError, stringifyError } from '@backstage/errors';
import { InputError, stringifyError, toError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import {
@@ -228,12 +228,12 @@ export class TaskWorker {
await task.complete('completed', { output });
await auditorEvent?.success();
} catch (error) {
assertError(error);
const err = toError(error);
await auditorEvent?.fail({
error,
error: err,
});
await task.complete('failed', {
error: { name: error.name, message: error.message },
error: { name: err.name, message: err.message },
});
}
}
@@ -27,7 +27,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, InputError, NotFoundError } from '@backstage/errors';
import { InputError, NotFoundError, toError } from '@backstage/errors';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import fs from 'fs-extra';
@@ -47,13 +47,13 @@ export async function getWorkingDirectory(
await fs.access(workingDirectory, fs.constants.F_OK | fs.constants.W_OK);
logger.info(`using working directory: ${workingDirectory}`);
} catch (err) {
assertError(err);
const error = toError(err);
logger.error(
`working directory ${workingDirectory} ${
err.code === 'ENOENT' ? 'does not exist' : 'is not writable'
error.code === 'ENOENT' ? 'does not exist' : 'is not writable'
}`,
);
throw err;
throw error;
}
return workingDirectory;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { IndexableDocument } from '@backstage/plugin-search-common';
import { Writable } from 'node:stream';
@@ -66,8 +66,7 @@ export abstract class BatchSearchEngineIndexer extends Writable {
await this.initialize();
done();
} catch (e) {
assertError(e);
done(e);
done(toError(e));
}
}
@@ -91,8 +90,7 @@ export abstract class BatchSearchEngineIndexer extends Writable {
this.currentBatch = [];
done();
} catch (e) {
assertError(e);
done(e);
done(toError(e));
}
}
@@ -110,8 +108,7 @@ export abstract class BatchSearchEngineIndexer extends Writable {
await this.finalize();
done();
} catch (e) {
assertError(e);
done(e);
done(toError(e));
}
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { IndexableDocument } from '@backstage/plugin-search-common';
import { Transform } from 'node:stream';
@@ -60,8 +60,7 @@ export abstract class DecoratorBase extends Transform {
await this.initialize();
done();
} catch (e) {
assertError(e);
done(e);
done(toError(e));
}
}
@@ -96,8 +95,7 @@ export abstract class DecoratorBase extends Transform {
this.push(decorated);
done();
} catch (e) {
assertError(e);
done(e);
done(toError(e));
}
}
@@ -110,8 +108,7 @@ export abstract class DecoratorBase extends Transform {
await this.finalize();
done();
} catch (e) {
assertError(e);
done(e);
done(toError(e));
}
}
}
@@ -19,7 +19,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, isError } from '@backstage/errors';
import { isError, toError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
GeneratorBase,
@@ -225,9 +225,8 @@ export class DocsBuilder {
// Not a blocker hence no need to await this.
fs.remove(preparedDir);
} catch (error) {
assertError(error);
this.logger.debug(
`Error removing prepared directory ${error.message}`,
`Error removing prepared directory ${toError(error).message}`,
);
}
}
@@ -238,9 +237,8 @@ export class DocsBuilder {
// Not a blocker hence no need to await this.
fs.remove(outputDir);
} catch (error) {
assertError(error);
this.logger.debug(
`Error removing generated directory ${error.message}`,
`Error removing generated directory ${toError(error).message}`,
);
}
}
+4 -4
View File
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assertError, CustomErrorBase } from '@backstage/errors';
import { CustomErrorBase, toError } from '@backstage/errors';
import { Config, readDurationFromConfig } from '@backstage/config';
import { CacheService, LoggerService } from '@backstage/backend-plugin-api';
import { durationToMilliseconds } from '@backstage/types';
@@ -77,9 +77,9 @@ export class TechDocsCache {
this.logger.debug(`Cache miss: ${path}`);
return response;
} catch (e) {
assertError(e);
this.logger.warn(`Error getting cache entry ${path}: ${e.message}`);
this.logger.debug(e.message, e);
const error = toError(e);
this.logger.warn(`Error getting cache entry ${path}: ${error.message}`);
this.logger.debug(error.message, error);
return undefined;
}
}
@@ -20,7 +20,7 @@ import {
stringifyEntityRef,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, NotFoundError } from '@backstage/errors';
import { NotFoundError, toError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import {
GeneratorBuilder,
@@ -146,13 +146,13 @@ export class DocsSynchronizer {
return;
}
} catch (e) {
assertError(e);
const buildError = toError(e);
const msg = `Failed to build the docs page for entity ${stringifyEntityRef(
entity,
)}: ${e.message}`;
)}: ${buildError.message}`;
taskLogger.error(msg);
this.logger.error(msg, e);
error(e);
this.logger.error(msg, buildError);
error(buildError);
return;
}
@@ -241,10 +241,9 @@ export class DocsSynchronizer {
finish({ updated: false });
}
} catch (e) {
assertError(e);
// In case of error, log and allow the user to go about their business.
this.logger.error(
`Error syncing cache for ${entityTripletPath}: ${e.message}`,
`Error syncing cache for ${entityTripletPath}: ${toError(e).message}`,
);
finish({ updated: false });
} finally {
@@ -15,9 +15,8 @@
*/
import { isChildPath, LoggerService } from '@backstage/backend-plugin-api';
import { NotAllowedError } from '@backstage/errors';
import { Entity } from '@backstage/catalog-model';
import { assertError, ForwardedError } from '@backstage/errors';
import { ForwardedError, NotAllowedError, toError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { SpawnOptionsWithoutStdio, spawn } from 'node:child_process';
import fs from 'fs-extra';
@@ -442,8 +441,9 @@ export const createOrUpdateMetadata = async (
try {
json = await fs.readJson(techdocsMetadataPath);
} catch (err) {
assertError(err);
const message = `Invalid JSON at ${techdocsMetadataPath} with error ${err.message}`;
const message = `Invalid JSON at ${techdocsMetadataPath} with error ${
toError(err).message
}`;
logger.error(message);
throw new Error(message);
}
@@ -457,9 +457,10 @@ export const createOrUpdateMetadata = async (
file.replace(`${techdocsMetadataDir}${path.sep}`, ''),
);
} catch (err) {
assertError(err);
json.files = [];
logger.warn(`Unable to add files list to metadata: ${err.message}`);
logger.warn(
`Unable to add files list to metadata: ${toError(err).message}`,
);
}
await fs.writeJson(techdocsMetadataPath, json);
@@ -21,7 +21,7 @@ import {
getRepoUrlFromLocationAnnotation,
MKDOCS_SCHEMA,
} from './helpers';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { LoggerService } from '@backstage/backend-plugin-api';
@@ -45,9 +45,10 @@ const patchMkdocsFile = async (
try {
mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8');
} catch (error) {
assertError(error);
logger.warn(
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`,
`Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${
toError(error).message
}`,
);
return;
}
@@ -62,9 +63,10 @@ const patchMkdocsFile = async (
throw new Error('Bad YAML format.');
}
} catch (error) {
assertError(error);
logger.warn(
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`,
`Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${
toError(error).message
}`,
);
return;
}
@@ -80,9 +82,10 @@ const patchMkdocsFile = async (
);
}
} catch (error) {
assertError(error);
logger.warn(
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`,
`Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${
toError(error).message
}`,
);
return;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { Entity } from '@backstage/catalog-model';
import { getDocFilesFromRepository } from '../../helpers';
import {
@@ -62,13 +62,13 @@ export class UrlPreparer implements PreparerBase {
logger: this.logger,
});
} catch (error) {
assertError(error);
const err = toError(error);
// NotModifiedError means that etag based cache is still valid.
if (error.name === 'NotModifiedError') {
if (err.name === 'NotModifiedError') {
this.logger.debug(`Cache is valid for etag ${options?.etag}`);
} else {
this.logger.debug(
`Unable to fetch files for building docs ${error.message}`,
`Unable to fetch files for building docs ${err.message}`,
);
}
@@ -15,7 +15,7 @@
*/
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, ForwardedError } from '@backstage/errors';
import { ForwardedError, toError } from '@backstage/errors';
// Maximum size in bytes for a single upload part (5MB)
const MAX_SINGLE_UPLOAD_BYTES = 5 * 1024 * 1024;
@@ -491,9 +491,10 @@ export class AwsS3Publish implements PublisherBase {
.map(f => f.Key || '')
.filter(f => !!f);
} catch (e) {
assertError(e);
this.logger.error(
`Unable to list files for Entity ${entity.metadata.name}: ${e.message}`,
`Unable to list files for Entity ${entity.metadata.name}: ${
toError(e).message
}`,
);
}
@@ -708,9 +709,8 @@ export class AwsS3Publish implements PublisherBase {
resolve(techdocsMetadata);
} catch (err) {
assertError(err);
this.logger.error(err.message);
reject(new Error(err.message));
this.logger.error(toError(err).message);
reject(new Error(toError(err).message));
}
});
} catch (e) {
@@ -769,9 +769,10 @@ export class AwsS3Publish implements PublisherBase {
})
.pipe(res);
} catch (err) {
assertError(err);
this.logger.warn(
`TechDocs S3 router failed to serve static files from bucket ${this.bucketName} at key ${filePath}: ${err.message}`,
`TechDocs S3 router failed to serve static files from bucket ${
this.bucketName
} at key ${filePath}: ${toError(err).message}`,
);
res.status(404).send('File Not Found');
}
@@ -823,8 +824,7 @@ export class AwsS3Publish implements PublisherBase {
try {
newPath = lowerCaseEntityTripletInStoragePath(file);
} catch (e) {
assertError(e);
this.logger.warn(e.message);
this.logger.warn(toError(e).message);
return;
}
@@ -852,8 +852,9 @@ export class AwsS3Publish implements PublisherBase {
);
}
} catch (e) {
assertError(e);
this.logger.warn(`Unable to migrate ${file}: ${e.message}`);
this.logger.warn(
`Unable to migrate ${file}: ${toError(e).message}`,
);
}
}, f),
),
@@ -21,7 +21,7 @@ import {
} from '@azure/storage-blob';
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, ForwardedError } from '@backstage/errors';
import { ForwardedError, toError } from '@backstage/errors';
import express from 'express';
import JSON5 from 'json5';
import limiterFactory from 'p-limit';
@@ -152,8 +152,9 @@ export class AzureBlobStoragePublish implements PublisherBase {
);
}
} catch (e) {
assertError(e);
this.logger.error(`from Azure Blob Storage client library: ${e.message}`);
this.logger.error(
`from Azure Blob Storage client library: ${toError(e).message}`,
);
}
this.logger.error(
@@ -190,9 +191,10 @@ export class AzureBlobStoragePublish implements PublisherBase {
maxPageSize: BATCH_CONCURRENCY,
});
} catch (e) {
assertError(e);
this.logger.error(
`Unable to list files for Entity ${entity.metadata.name}: ${e.message}`,
`Unable to list files for Entity ${entity.metadata.name}: ${
toError(e).message
}`,
);
}
@@ -421,8 +423,7 @@ export class AzureBlobStoragePublish implements PublisherBase {
try {
newPath = lowerCaseEntityTripletInStoragePath(originalPath);
} catch (e) {
assertError(e);
this.logger.warn(e.message);
this.logger.warn(toError(e).message);
return;
}
@@ -431,8 +432,9 @@ export class AzureBlobStoragePublish implements PublisherBase {
this.logger.debug(`Migrating ${originalPath}`);
await this.renameBlob(originalPath, newPath, removeOriginal);
} catch (e) {
assertError(e);
this.logger.warn(`Unable to migrate ${originalPath}: ${e.message}`);
this.logger.warn(
`Unable to migrate ${originalPath}: ${toError(e).message}`,
);
}
}
@@ -15,7 +15,7 @@
*/
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import {
File,
FileExistsResponse,
@@ -149,14 +149,13 @@ export class GoogleGCSPublish implements PublisherBase {
isAvailable: true,
};
} catch (err) {
assertError(err);
this.logger.error(
`Could not retrieve metadata about the GCS bucket ${this.bucketName}. ` +
'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' +
'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' +
'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
);
this.logger.error(`from GCS client library: ${err.message}`);
this.logger.error(`from GCS client library: ${toError(err).message}`);
return { isAvailable: false };
}
@@ -186,9 +185,10 @@ export class GoogleGCSPublish implements PublisherBase {
);
existingFiles = await this.getFilesForFolder(remoteFolder);
} catch (e) {
assertError(e);
this.logger.error(
`Unable to list files for Entity ${entity.metadata.name}: ${e.message}`,
`Unable to list files for Entity ${entity.metadata.name}: ${
toError(e).message
}`,
);
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { assertError } from '@backstage/errors';
import { toError } from '@backstage/errors';
import { File } from '@google-cloud/storage';
import { Writable } from 'node:stream';
import { lowerCaseEntityTripletInStoragePath } from '../helpers';
@@ -47,8 +47,7 @@ export class MigrateWriteStream extends Writable {
try {
newFile = lowerCaseEntityTripletInStoragePath(file.name);
} catch (e) {
assertError(e);
this.logger.warn(e.message);
this.logger.warn(toError(e).message);
next();
return;
}
@@ -36,7 +36,7 @@ import {
ReadinessResponse,
TechDocsMetadata,
} from './types';
import { assertError, ForwardedError } from '@backstage/errors';
import { ForwardedError, toError } from '@backstage/errors';
import { LoggerService } from '@backstage/backend-plugin-api';
const streamToBuffer = (stream: Stream | Readable): Promise<Buffer> => {
@@ -129,8 +129,9 @@ export class OpenStackSwiftPublish implements PublisherBase {
isAvailable: false,
};
} catch (err) {
assertError(err);
this.logger.error(`from OpenStack client library: ${err.message}`);
this.logger.error(
`from OpenStack client library: ${toError(err).message}`,
);
return {
isAvailable: false,
};
@@ -221,9 +222,8 @@ export class OpenStackSwiftPublish implements PublisherBase {
resolve(techdocsMetadata);
} catch (err) {
assertError(err);
this.logger.error(err.message);
reject(new Error(err.message));
this.logger.error(toError(err).message);
reject(new Error(toError(err).message));
}
} else {
reject({
@@ -264,9 +264,10 @@ export class OpenStackSwiftPublish implements PublisherBase {
res.send(await streamToBuffer(stream));
} catch (err) {
assertError(err);
this.logger.warn(
`TechDocs OpenStack swift router failed to serve content from container ${this.containerName} at path ${filePath}: ${err.message}`,
`TechDocs OpenStack swift router failed to serve content from container ${
this.containerName
} at path ${filePath}: ${toError(err).message}`,
);
res.status(404).send('File Not Found');
}
@@ -296,8 +297,7 @@ export class OpenStackSwiftPublish implements PublisherBase {
}
return false;
} catch (err) {
assertError(err);
this.logger.warn(err.message);
this.logger.warn(toError(err).message);
return false;
}
}
@@ -316,8 +316,7 @@ export class OpenStackSwiftPublish implements PublisherBase {
try {
newPath = lowerCaseEntityTripletInStoragePath(file);
} catch (e) {
assertError(e);
this.logger.warn(e.message);
this.logger.warn(toError(e).message);
return;
}
@@ -338,8 +337,9 @@ export class OpenStackSwiftPublish implements PublisherBase {
await this.storageClient.delete(this.containerName, file);
}
} catch (e) {
assertError(e);
this.logger.warn(`Unable to migrate ${file}: ${e.message}`);
this.logger.warn(
`Unable to migrate ${file}: ${toError(e).message}`,
);
}
}, f),
),