diff --git a/.changeset/chilled-kiwis-bathe.md b/.changeset/chilled-kiwis-bathe.md
new file mode 100644
index 0000000000..ef1517a58c
--- /dev/null
+++ b/.changeset/chilled-kiwis-bathe.md
@@ -0,0 +1,5 @@
+---
+'@backstage/catalog-client': patch
+---
+
+Throw the new `ResponseError` from `@backstage/errors`
diff --git a/.changeset/dirty-flowers-fail.md b/.changeset/dirty-flowers-fail.md
new file mode 100644
index 0000000000..4a3baabcb4
--- /dev/null
+++ b/.changeset/dirty-flowers-fail.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-graphiql': patch
+---
+
+Export `GraphiQLIcon`.
diff --git a/.changeset/fair-students-buy.md b/.changeset/fair-students-buy.md
new file mode 100644
index 0000000000..600c86e6c1
--- /dev/null
+++ b/.changeset/fair-students-buy.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-user-settings': patch
+---
+
+Avoid using `ApiRef` descriptions in the UI.
diff --git a/.changeset/healthy-cars-do.md b/.changeset/healthy-cars-do.md
new file mode 100644
index 0000000000..7f3b874ba3
--- /dev/null
+++ b/.changeset/healthy-cars-do.md
@@ -0,0 +1,31 @@
+---
+'@backstage/backend-common': minor
+---
+
+Encode thrown errors in the backend as a JSON payload. This is technically a breaking change, since the response format even of errors are part of the contract. If you relied on the response being text, you will now have some extra JSON "noise" in it. It should still be readable by end users though.
+
+Before:
+
+```
+NotFoundError: No entity named 'tara.macgovern2' found, with kind 'user' in namespace 'default'
+ at eval (webpack-internal:///../../plugins/catalog-backend/src/service/router.ts:117:17)
+```
+
+After:
+
+```json
+{
+ "error": {
+ "name": "NotFoundError",
+ "message": "No entity named 'tara.macgovern2' found, with kind 'user' in namespace 'default'",
+ "stack": "NotFoundError: No entity named 'tara.macgovern2' found, with kind 'user' in namespace 'default'\n at eval (webpack-internal:///../../plugins/catalog-backend/src/service/router.ts:117:17)"
+ },
+ "request": {
+ "method": "GET",
+ "url": "/entities/by-name/user/default/tara.macgovern2"
+ },
+ "response": {
+ "statusCode": 404
+ }
+}
+```
diff --git a/.changeset/khaki-avocados-remain.md b/.changeset/khaki-avocados-remain.md
new file mode 100644
index 0000000000..b4d8f7a925
--- /dev/null
+++ b/.changeset/khaki-avocados-remain.md
@@ -0,0 +1,5 @@
+---
+'@backstage/cli': patch
+---
+
+Lint storybook files, i.e. `*.stories.*`, as if they were tests.
diff --git a/.changeset/olive-apples-shop.md b/.changeset/olive-apples-shop.md
new file mode 100644
index 0000000000..754c926780
--- /dev/null
+++ b/.changeset/olive-apples-shop.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core': patch
+---
+
+Add a `ResponseErrorPanel` to render `ResponseError` from `@backstage/errors`
diff --git a/.changeset/silver-weeks-hide.md b/.changeset/silver-weeks-hide.md
new file mode 100644
index 0000000000..15a75016a2
--- /dev/null
+++ b/.changeset/silver-weeks-hide.md
@@ -0,0 +1,10 @@
+---
+'@backstage/techdocs-common': patch
+'@backstage/plugin-auth-backend': patch
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-catalog-backend': patch
+'@backstage/plugin-scaffolder-backend': patch
+'@backstage/plugin-techdocs-backend': patch
+---
+
+Use errors from `@backstage/errors`
diff --git a/.changeset/young-suits-sit.md b/.changeset/young-suits-sit.md
new file mode 100644
index 0000000000..5728c52ef8
--- /dev/null
+++ b/.changeset/young-suits-sit.md
@@ -0,0 +1,10 @@
+---
+'@backstage/backend-common': minor
+---
+
+Removed the custom error types (e.g. `NotFoundError`). Those are now instead in the new `@backstage/errors` package. This is a breaking change, and you will have to update your imports if you were using these types.
+
+```diff
+-import { NotFoundError } from '@backstage/backend-common';
++import { NotFoundError } from '@backstage/errors';
+```
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 80706fe459..7c33113e83 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -36,7 +36,7 @@ import {
SidebarSpace,
} from '@backstage/core';
import { NavLink } from 'react-router-dom';
-import { graphiQLRouteRef } from '@backstage/plugin-graphiql';
+import { GraphiQLIcon } from '@backstage/plugin-graphiql';
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
import { SidebarSearch } from '@backstage/plugin-search';
@@ -90,11 +90,7 @@ const Root = ({ children }: PropsWithChildren<{}>) => (
-
+
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 43b5114de4..933d2ccb3a 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -32,6 +32,7 @@
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.2",
"@backstage/config-loader": "^0.5.1",
+ "@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@octokit/rest": "^18.0.12",
"@types/cors": "^2.8.6",
diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts
index f3653f2627..29b1062750 100644
--- a/packages/backend-common/src/index.ts
+++ b/packages/backend-common/src/index.ts
@@ -17,7 +17,6 @@
export * from './config';
export * from './database';
export * from './discovery';
-export * from './errors';
export * from './hot';
export * from './logging';
export * from './middleware';
diff --git a/packages/backend-common/src/middleware/errorHandler.test.ts b/packages/backend-common/src/middleware/errorHandler.test.ts
index 78baaf5090..3fb7aedc37 100644
--- a/packages/backend-common/src/middleware/errorHandler.test.ts
+++ b/packages/backend-common/src/middleware/errorHandler.test.ts
@@ -14,10 +14,17 @@
* limitations under the License.
*/
+import {
+ AuthenticationError,
+ ConflictError,
+ InputError,
+ NotAllowedError,
+ NotFoundError,
+ NotModifiedError,
+} from '@backstage/errors';
import express from 'express';
import createError from 'http-errors';
import request from 'supertest';
-import * as errors from '../errors';
import { errorHandler } from './errorHandler';
describe('errorHandler', () => {
@@ -31,17 +38,24 @@ describe('errorHandler', () => {
const response = await request(app).get('/breaks');
expect(response.status).toBe(500);
- expect(response.text).toBe('some message');
+ expect(response.body).toEqual({
+ error: expect.objectContaining({
+ name: 'Error',
+ message: 'some message',
+ }),
+ request: { method: 'GET', url: '/breaks' },
+ response: { statusCode: 500 },
+ });
});
- it('doesnt try to send the response again if its already been sent', async () => {
+ it('does not try to send the response again if its already been sent', async () => {
const app = express();
const mockSend = jest.fn();
app.use('/works_with_async_fail', (_, res) => {
res.status(200).send('hello');
- // mutate the response object to test the middlware.
+ // mutate the response object to test the middleware.
// it's hard to catch errors inside middleware from the outside.
// @ts-ignore
res.send = mockSend;
@@ -67,38 +81,64 @@ describe('errorHandler', () => {
const response = await request(app).get('/breaks');
expect(response.status).toBe(432);
- expect(response.text).toContain('Some Message');
+ expect(response.body).toEqual({
+ error: {
+ expose: true,
+ name: 'BadRequestError',
+ message: 'Some Message',
+ status: 432,
+ statusCode: 432,
+ },
+ request: {
+ method: 'GET',
+ url: '/breaks',
+ },
+ response: { statusCode: 432 },
+ });
});
it('handles well-known error classes', async () => {
const app = express();
app.use('/NotModifiedError', () => {
- throw new errors.NotModifiedError();
+ throw new NotModifiedError();
});
app.use('/InputError', () => {
- throw new errors.InputError();
+ throw new InputError();
});
app.use('/AuthenticationError', () => {
- throw new errors.AuthenticationError();
+ throw new AuthenticationError();
});
app.use('/NotAllowedError', () => {
- throw new errors.NotAllowedError();
+ throw new NotAllowedError();
});
app.use('/NotFoundError', () => {
- throw new errors.NotFoundError();
+ throw new NotFoundError();
});
app.use('/ConflictError', () => {
- throw new errors.ConflictError();
+ throw new ConflictError();
});
app.use(errorHandler());
const r = request(app);
expect((await r.get('/NotModifiedError')).status).toBe(304);
expect((await r.get('/InputError')).status).toBe(400);
+ expect((await r.get('/InputError')).body.error.name).toBe('InputError');
expect((await r.get('/AuthenticationError')).status).toBe(401);
+ expect((await r.get('/AuthenticationError')).body.error.name).toBe(
+ 'AuthenticationError',
+ );
expect((await r.get('/NotAllowedError')).status).toBe(403);
+ expect((await r.get('/NotAllowedError')).body.error.name).toBe(
+ 'NotAllowedError',
+ );
expect((await r.get('/NotFoundError')).status).toBe(404);
+ expect((await r.get('/NotFoundError')).body.error.name).toBe(
+ 'NotFoundError',
+ );
expect((await r.get('/ConflictError')).status).toBe(409);
+ expect((await r.get('/ConflictError')).body.error.name).toBe(
+ 'ConflictError',
+ );
});
it('logs all 500 errors', async () => {
@@ -126,7 +166,7 @@ describe('errorHandler', () => {
mockLogger.child.mockImplementation(() => mockLogger as any);
app.use('/NotFound', () => {
- throw new errors.NotFoundError();
+ throw new NotFoundError();
});
app.use(errorHandler({ logger: mockLogger as any }));
@@ -142,7 +182,7 @@ describe('errorHandler', () => {
mockLogger.child.mockImplementation(() => mockLogger as any);
app.use('/NotFound', () => {
- throw new errors.NotFoundError();
+ throw new NotFoundError();
});
app.use(errorHandler({ logger: mockLogger as any, logClientErrors: true }));
diff --git a/packages/backend-common/src/middleware/errorHandler.ts b/packages/backend-common/src/middleware/errorHandler.ts
index 4b609a649f..bee3f5557d 100644
--- a/packages/backend-common/src/middleware/errorHandler.ts
+++ b/packages/backend-common/src/middleware/errorHandler.ts
@@ -14,9 +14,18 @@
* limitations under the License.
*/
+import {
+ AuthenticationError,
+ ConflictError,
+ ErrorResponse,
+ InputError,
+ NotAllowedError,
+ NotFoundError,
+ NotModifiedError,
+ serializeError,
+} from '@backstage/errors';
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
import { Logger } from 'winston';
-import * as errors from '../errors';
import { getRootLogger } from '../logging';
export type ErrorHandlerOptions = {
@@ -48,14 +57,13 @@ export type ErrorHandlerOptions = {
* This is commonly the very last middleware in the chain.
*
* Its primary purpose is not to do translation of business logic exceptions,
- * but rather to be a gobal catch-all for uncaught "fatal" errors that are
+ * but rather to be a global catch-all for uncaught "fatal" errors that are
* expected to result in a 500 error. However, it also does handle some common
* error types (such as http-error exceptions) and returns the enclosed status
* code accordingly.
*
* @returns An Express error request handler
*/
-
export function errorHandler(
options: ErrorHandlerOptions = {},
): ErrorRequestHandler {
@@ -66,12 +74,12 @@ export function errorHandler(
type: 'errorHandler',
});
- return (
- error: Error,
- _request: Request,
- res: Response,
- next: NextFunction,
- ) => {
+ return (error: Error, req: Request, res: Response, next: NextFunction) => {
+ const statusCode = getStatusCode(error);
+ if (options.logClientErrors || statusCode >= 500) {
+ logger.error(error);
+ }
+
if (res.headersSent) {
// If the headers have already been sent, do not send the response again
// as this will throw an error in the backend.
@@ -79,16 +87,13 @@ export function errorHandler(
return;
}
- const status = getStatusCode(error);
- const message = showStackTraces ? error.stack : error.message;
+ const body: ErrorResponse = {
+ error: serializeError(error, { includeStack: showStackTraces }),
+ request: { method: req.method, url: req.url },
+ response: { statusCode },
+ };
- if (options.logClientErrors || status >= 500) {
- logger.error(error);
- }
-
- res.status(status);
- res.setHeader('content-type', 'text/plain');
- res.send(message);
+ res.status(statusCode).json(body);
};
}
@@ -109,17 +114,17 @@ function getStatusCode(error: Error): number {
// Handle well-known error types
switch (error.name) {
- case errors.NotModifiedError.name:
+ case NotModifiedError.name:
return 304;
- case errors.InputError.name:
+ case InputError.name:
return 400;
- case errors.AuthenticationError.name:
+ case AuthenticationError.name:
return 401;
- case errors.NotAllowedError.name:
+ case NotAllowedError.name:
return 403;
- case errors.NotFoundError.name:
+ case NotFoundError.name:
return 404;
- case errors.ConflictError.name:
+ case ConflictError.name:
return 409;
default:
break;
diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts
index b52097baf5..bb76181287 100644
--- a/packages/backend-common/src/reading/AzureUrlReader.test.ts
+++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts
@@ -26,7 +26,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import * as os from 'os';
import path from 'path';
-import { NotModifiedError } from '../errors';
+import { NotModifiedError } from '@backstage/errors';
import { getVoidLogger } from '../logging';
import { AzureUrlReader } from './AzureUrlReader';
import { ReadTreeResponseFactory } from './tree';
diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts
index 21b988e5b0..16e7628ec8 100644
--- a/packages/backend-common/src/reading/AzureUrlReader.ts
+++ b/packages/backend-common/src/reading/AzureUrlReader.ts
@@ -26,7 +26,7 @@ import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
-import { NotFoundError, NotModifiedError } from '../errors';
+import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
index df9febb352..aa78bf27d5 100644
--- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts
@@ -26,7 +26,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import os from 'os';
import path from 'path';
-import { NotModifiedError } from '../errors';
+import { NotModifiedError } from '@backstage/errors';
import { BitbucketUrlReader } from './BitbucketUrlReader';
import { ReadTreeResponseFactory } from './tree';
diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts
index 3954e7ee4a..f102be7283 100644
--- a/packages/backend-common/src/reading/BitbucketUrlReader.ts
+++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts
@@ -26,7 +26,7 @@ import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
-import { NotFoundError, NotModifiedError } from '../errors';
+import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts
index 0b26dfe911..57bab5e58d 100644
--- a/packages/backend-common/src/reading/FetchUrlReader.ts
+++ b/packages/backend-common/src/reading/FetchUrlReader.ts
@@ -15,7 +15,7 @@
*/
import fetch from 'cross-fetch';
-import { NotFoundError } from '../errors';
+import { NotFoundError } from '@backstage/errors';
import {
ReaderFactory,
ReadTreeResponse,
diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts
index 94e4d0fd31..67e7f27069 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts
@@ -27,7 +27,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import os from 'os';
import path from 'path';
-import { NotFoundError, NotModifiedError } from '../errors';
+import { NotFoundError, NotModifiedError } from '@backstage/errors';
import {
GhBlobResponse,
GhBranchResponse,
diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts
index ac5a93bd5f..77e33cbd54 100644
--- a/packages/backend-common/src/reading/GithubUrlReader.ts
+++ b/packages/backend-common/src/reading/GithubUrlReader.ts
@@ -25,7 +25,7 @@ import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
-import { NotFoundError, NotModifiedError } from '../errors';
+import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import {
ReaderFactory,
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
index 636540caa8..9dcc92a4a6 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
@@ -25,7 +25,7 @@ import path from 'path';
import { getVoidLogger } from '../logging';
import { GitlabUrlReader } from './GitlabUrlReader';
import { ReadTreeResponseFactory } from './tree';
-import { NotModifiedError, NotFoundError } from '../errors';
+import { NotModifiedError, NotFoundError } from '@backstage/errors';
import {
GitLabIntegration,
readGitLabIntegrationConfig,
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts
index 66ceae05c6..c93103b2da 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.ts
@@ -24,7 +24,7 @@ import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
-import { NotFoundError, NotModifiedError } from '../errors';
+import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts
index 91f4ae63ab..06ca0c5971 100644
--- a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts
+++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { NotAllowedError } from '../errors';
+import { NotAllowedError } from '@backstage/errors';
import {
ReadTreeOptions,
ReadTreeResponse,
diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json
index 9115a29ab3..36d63a4230 100644
--- a/packages/catalog-client/package.json
+++ b/packages/catalog-client/package.json
@@ -31,6 +31,7 @@
"dependencies": {
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.2",
+ "@backstage/errors": "^0.1.1",
"cross-fetch": "^3.0.6"
},
"devDependencies": {
diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts
index a1af0240f6..60e15906d3 100644
--- a/packages/catalog-client/src/CatalogClient.ts
+++ b/packages/catalog-client/src/CatalogClient.ts
@@ -21,6 +21,7 @@ import {
LOCATION_ANNOTATION,
stringifyLocationReference,
} from '@backstage/catalog-model';
+import { ResponseError } from '@backstage/errors';
import fetch from 'cross-fetch';
import {
AddLocationRequest,
@@ -153,10 +154,7 @@ export class CatalogClient implements CatalogApi {
},
);
if (!response.ok) {
- const payload = await response.text();
- throw new Error(
- `Request failed with ${response.status} ${response.statusText}, ${payload}`,
- );
+ throw await ResponseError.fromResponse(response);
}
return undefined;
}
@@ -177,9 +175,7 @@ export class CatalogClient implements CatalogApi {
});
if (!response.ok) {
- const payload = await response.text();
- const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
- throw new Error(message);
+ throw await ResponseError.fromResponse(response);
}
return await response.json();
@@ -200,10 +196,7 @@ export class CatalogClient implements CatalogApi {
if (response.status === 404) {
return undefined;
}
-
- const payload = await response.text();
- const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
- throw new Error(message);
+ throw await ResponseError.fromResponse(response);
}
return await response.json();
diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js
index 37bd7bed94..fe45f264b5 100644
--- a/packages/cli/config/eslint.js
+++ b/packages/cli/config/eslint.js
@@ -100,7 +100,7 @@ module.exports = {
},
},
{
- files: ['*.test.*', 'src/setupTests.*', 'dev/**'],
+ files: ['*.test.*', '*.stories.*', 'src/setupTests.*', 'dev/**'],
rules: {
// Tests are allowed to import dev dependencies
'import/no-extraneous-dependencies': [
diff --git a/packages/core/package.json b/packages/core/package.json
index 6eda51100f..4a95fe63e2 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -31,6 +31,7 @@
"dependencies": {
"@backstage/config": "^0.1.3",
"@backstage/core-api": "^0.2.13",
+ "@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.4",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx
index 9f6cdd7e7c..7f20aed496 100644
--- a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx
+++ b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx
@@ -14,21 +14,11 @@
* limitations under the License.
*/
-import React, { useRef, useState, MouseEventHandler } from 'react';
-import { IconButton, makeStyles, Tooltip } from '@material-ui/core';
-import PropTypes from 'prop-types';
-import CopyIcon from '@material-ui/icons/FileCopy';
-import { BackstageTheme } from '@backstage/theme';
import { errorApiRef, useApi } from '@backstage/core-api';
-
-const useStyles = makeStyles(theme => ({
- button: {
- '&:hover': {
- backgroundColor: theme.palette.highlight,
- cursor: 'pointer',
- },
- },
-}));
+import { IconButton, Tooltip } from '@material-ui/core';
+import CopyIcon from '@material-ui/icons/FileCopy';
+import PropTypes from 'prop-types';
+import React, { MouseEventHandler, useRef, useState } from 'react';
/**
* Copy text button with visual feedback in the form of
@@ -61,7 +51,6 @@ export const CopyTextButton = (props: Props) => {
...defaultProps,
...props,
};
- const classes = useStyles(props);
const errorApi = useApi(errorApiRef);
const inputRef = useRef(null);
const [open, setOpen] = useState(false);
@@ -84,7 +73,7 @@ export const CopyTextButton = (props: Props) => {
<>
{
onClose={() => setOpen(false)}
open={open}
>
-
+
diff --git a/packages/core/src/components/ResponseErrorPanel/ResponseErrorPanel.tsx b/packages/core/src/components/ResponseErrorPanel/ResponseErrorPanel.tsx
new file mode 100644
index 0000000000..c36be78f08
--- /dev/null
+++ b/packages/core/src/components/ResponseErrorPanel/ResponseErrorPanel.tsx
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ResponseError } from '@backstage/errors';
+import {
+ Divider,
+ List,
+ ListItem,
+ ListItemText,
+ makeStyles,
+} from '@material-ui/core';
+import React from 'react';
+import { CodeSnippet } from '../CodeSnippet';
+import { CopyTextButton } from '../CopyTextButton';
+import { WarningPanel } from '../WarningPanel';
+
+const useStyles = makeStyles(theme => ({
+ text: {
+ fontFamily: 'monospace',
+ whiteSpace: 'pre',
+ overflowX: 'auto',
+ marginRight: theme.spacing(2),
+ },
+ divider: {
+ margin: theme.spacing(2),
+ },
+}));
+
+type Props = {
+ error: Error;
+};
+
+/**
+ * Renders details about a failed server request.
+ *
+ * Has special treatment for ResponseError errors, to display rich
+ * server-provided information about what happened.
+ */
+export const ResponseErrorDetails = ({ error }: Props) => {
+ const classes = useStyles();
+
+ if (error.name !== 'ResponseError') {
+ return (
+
+
+
+ );
+ }
+
+ const { data, cause } = error as ResponseError;
+ const { request, response } = data;
+
+ const errorString = `${response.statusCode}: ${cause.name}`;
+ const requestString = request && `${request.method} ${request.url}`;
+ const messageString = cause.message.replace(/\\n/g, '\n');
+ const stackString = cause.stack?.replace(/\\n/g, '\n');
+ const jsonString = JSON.stringify(data, undefined, 2);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {requestString && (
+
+
+
+
+ )}
+ {stackString && (
+
+
+
+
+ )}
+
+
+ }
+ />
+
+
+
+ );
+};
+
+/**
+ * Renders a warning panel as the effect of a failed server request.
+ *
+ * Has special treatment for ResponseError errors, to display rich
+ * server-provided information about what happened.
+ */
+export const ResponseErrorPanel = ({ error }: Props) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/core/src/components/ResponseErrorPanel/index.ts b/packages/core/src/components/ResponseErrorPanel/index.ts
new file mode 100644
index 0000000000..ac62553fd5
--- /dev/null
+++ b/packages/core/src/components/ResponseErrorPanel/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ResponseErrorDetails, ResponseErrorPanel } from './ResponseErrorPanel';
diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts
index 58d0a8f569..c8797f576b 100644
--- a/packages/core/src/components/index.ts
+++ b/packages/core/src/components/index.ts
@@ -22,6 +22,7 @@ export * from './CopyTextButton';
export * from './DependencyGraph';
export * from './DismissableBanner';
export * from './EmptyState';
+export * from './ResponseErrorPanel';
export * from './FeatureDiscovery';
export * from './HeaderIconLinkRow';
export * from './HorizontalScrollGrid';
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index e04e5bae12..9016ce9563 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -50,6 +50,7 @@
"@backstage/cli": "*",
"@backstage/config": "*",
"@backstage/core": "*",
+ "@backstage/errors": "*",
"@backstage/plugin-api-docs": "*",
"@backstage/plugin-app-backend": "*",
"@backstage/plugin-auth-backend": "*",
diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts
index 9a05a95ac0..32a87bb323 100644
--- a/packages/create-app/src/lib/versions.ts
+++ b/packages/create-app/src/lib/versions.ts
@@ -36,6 +36,7 @@ import { version as catalogModel } from '../../../catalog-model/package.json';
import { version as cli } from '../../../cli/package.json';
import { version as config } from '../../../config/package.json';
import { version as core } from '../../../core/package.json';
+import { version as errors } from '../../../errors/package.json';
import { version as testUtils } from '../../../test-utils/package.json';
import { version as theme } from '../../../theme/package.json';
@@ -67,6 +68,7 @@ export const packageVersions = {
'@backstage/cli': cli,
'@backstage/config': config,
'@backstage/core': core,
+ '@backstage/errors': errors,
'@backstage/plugin-api-docs': pluginApiDocs,
'@backstage/plugin-app-backend': pluginAppBackend,
'@backstage/plugin-auth-backend': pluginAuthBackend,
diff --git a/packages/errors/.eslintrc.js b/packages/errors/.eslintrc.js
new file mode 100644
index 0000000000..13573efa9c
--- /dev/null
+++ b/packages/errors/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: [require.resolve('@backstage/cli/config/eslint')],
+};
diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/errors/README.md b/packages/errors/README.md
new file mode 100644
index 0000000000..8018e89980
--- /dev/null
+++ b/packages/errors/README.md
@@ -0,0 +1,9 @@
+# Common error handling functionality
+
+Contains some common functionality of error handling.
+
+This package will be imported both by the frontend and backend.
+
+## Links
+
+- [The Backstage homepage](https://backstage.io)
diff --git a/packages/errors/package.json b/packages/errors/package.json
new file mode 100644
index 0000000000..508cf4fab0
--- /dev/null
+++ b/packages/errors/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "@backstage/errors",
+ "version": "0.1.1",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": false,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "module": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "packages/errors"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "scripts": {
+ "build": "backstage-cli build",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/config": "^0.1.2",
+ "cross-fetch": "^3.0.6",
+ "serialize-error": "^8.0.1"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.6.1",
+ "@types/jest": "^26.0.7"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/packages/errors/src/errors/CustomErrorBase.ts b/packages/errors/src/errors/CustomErrorBase.ts
new file mode 100644
index 0000000000..cd28d4cda3
--- /dev/null
+++ b/packages/errors/src/errors/CustomErrorBase.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export class CustomErrorBase extends Error {
+ readonly cause?: Error;
+
+ constructor(message?: string, cause?: Error) {
+ let fullMessage = message;
+ if (cause) {
+ if (fullMessage) {
+ fullMessage += `; caused by ${cause}`;
+ } else {
+ fullMessage = `caused by ${cause}`;
+ }
+ }
+
+ super(fullMessage);
+
+ Error.captureStackTrace?.(this, this.constructor);
+
+ this.name = this.constructor.name;
+ this.cause = cause;
+ }
+}
diff --git a/packages/errors/src/errors/ResponseError.test.ts b/packages/errors/src/errors/ResponseError.test.ts
new file mode 100644
index 0000000000..a67fb8bbfa
--- /dev/null
+++ b/packages/errors/src/errors/ResponseError.test.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ErrorResponse } from '../serialization';
+import { ResponseError } from './ResponseError';
+
+describe('ResponseError', () => {
+ it('constructs itself from a response', async () => {
+ const body: ErrorResponse = {
+ error: { name: 'Fours', message: 'Expected fives', stack: 'lines' },
+ request: { method: 'GET', url: '/' },
+ response: { statusCode: 444 },
+ };
+
+ const response: Partial = {
+ status: 444,
+ statusText: 'Fours',
+ text: async () => JSON.stringify(body),
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ };
+
+ const e = await ResponseError.fromResponse(response as Response);
+ expect(e.name).toEqual('ResponseError');
+ expect(e.message).toEqual('Request failed with 444 Fours');
+ expect(e.cause.name).toEqual('Fours');
+ expect(e.cause.message).toEqual('Expected fives');
+ expect(e.cause.stack).toEqual('lines');
+ });
+});
diff --git a/packages/errors/src/errors/ResponseError.ts b/packages/errors/src/errors/ResponseError.ts
new file mode 100644
index 0000000000..0fff8afa66
--- /dev/null
+++ b/packages/errors/src/errors/ResponseError.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ parseErrorResponse,
+ ErrorResponse,
+ deserializeError,
+} from '../serialization';
+
+/**
+ * An error thrown as the result of a failed server request.
+ *
+ * The server is expected to respond on the ErrorResponse format.
+ */
+export class ResponseError extends Error {
+ /**
+ * The actual response, as seen by the client.
+ *
+ * Note that the body of this response is always consumed. Its parsed form is
+ * in the `body` field.
+ */
+ readonly response: Response;
+
+ /**
+ * The parsed JSON error body, as sent by the server.
+ */
+ readonly data: ErrorResponse;
+
+ /**
+ * The Error cause, as seen by the remote server. This is parsed out of the
+ * JSON error body.
+ *
+ * This error always has the plain Error constructor, however all
+ * serializable enumerable fields on the remote error including its name are
+ * preserved. Therefore, if you want to check the error type, use its name
+ * property rather than checking typeof or its constructor or prototype.
+ */
+ readonly cause: Error;
+
+ /**
+ * Constructs a ResponseError based on a failed response.
+ *
+ * Assumes that the response has already been checked to be not ok. This
+ * function consumes the body of the response, and assumes that it hasn't
+ * been consumed before.
+ */
+ static async fromResponse(response: Response): Promise {
+ const data = await parseErrorResponse(response);
+
+ const status = data.response.statusCode || response.status;
+ const statusText = data.error.name || response.statusText;
+ const message = `Request failed with ${status} ${statusText}`;
+ const cause = deserializeError(data.error);
+
+ return new ResponseError({
+ message,
+ response,
+ data,
+ cause,
+ });
+ }
+
+ constructor(props: {
+ message: string;
+ response: Response;
+ data: ErrorResponse;
+ cause: Error;
+ }) {
+ super(props.message);
+ this.name = 'ResponseError';
+ this.response = props.response;
+ this.data = props.data;
+ this.cause = props.cause;
+ }
+}
diff --git a/packages/backend-common/src/errors.test.ts b/packages/errors/src/errors/common.test.ts
similarity index 91%
rename from packages/backend-common/src/errors.test.ts
rename to packages/errors/src/errors/common.test.ts
index ecff8764c0..97e616c718 100644
--- a/packages/backend-common/src/errors.test.ts
+++ b/packages/errors/src/errors/common.test.ts
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-import * as errors from './errors';
+import * as errors from './common';
-describe('errors', () => {
+describe('common', () => {
it('extends Error properly', () => {
for (const [name, E] of Object.entries(errors)) {
const error = new E('abcdef');
@@ -30,7 +30,7 @@ describe('errors', () => {
it('supports causes', () => {
const cause = new Error('hello');
- for (const [, E] of Object.entries(errors)) {
+ for (const [name, E] of Object.entries(errors)) {
const error = new E('abcdef', cause);
expect(error.cause).toBe(cause);
expect(error.toString()).toContain(
diff --git a/packages/backend-common/src/errors.ts b/packages/errors/src/errors/common.ts
similarity index 76%
rename from packages/backend-common/src/errors.ts
rename to packages/errors/src/errors/common.ts
index 39703127e6..60fa540355 100644
--- a/packages/backend-common/src/errors.ts
+++ b/packages/errors/src/errors/common.ts
@@ -14,39 +14,19 @@
* limitations under the License.
*/
+import { CustomErrorBase } from './CustomErrorBase';
+
/*
* A set of common business logic errors.
*
- * The error handler middleware understands these and will translate them to
- * well formed HTTP responses.
+ * A backend error handler middleware would understand these and translate them
+ * to well formed HTTP responses.
*
* While these are intentionally analogous to HTTP errors, they are not
* intended to be thrown by the request handling layer. In those places, please
* use e.g. the http-errors library.
*/
-class CustomErrorBase extends Error {
- readonly cause?: Error;
-
- constructor(message?: string, cause?: Error) {
- let fullMessage = message;
- if (cause) {
- if (fullMessage) {
- fullMessage += `; caused by ${cause}`;
- } else {
- fullMessage = `caused by ${cause}`;
- }
- }
-
- super(fullMessage);
-
- Error.captureStackTrace(this, this.constructor);
-
- this.name = this.constructor.name;
- this.cause = cause;
- }
-}
-
/**
* The given inputs are malformed and cannot be processed.
*/
diff --git a/packages/errors/src/errors/index.ts b/packages/errors/src/errors/index.ts
new file mode 100644
index 0000000000..f48aacf881
--- /dev/null
+++ b/packages/errors/src/errors/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {
+ AuthenticationError,
+ ConflictError,
+ InputError,
+ NotAllowedError,
+ NotFoundError,
+ NotModifiedError,
+} from './common';
+export { CustomErrorBase } from './CustomErrorBase';
+export { ResponseError } from './ResponseError';
diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts
new file mode 100644
index 0000000000..a8d2142d88
--- /dev/null
+++ b/packages/errors/src/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './errors';
+export * from './serialization';
diff --git a/packages/errors/src/serialization/error.test.ts b/packages/errors/src/serialization/error.test.ts
new file mode 100644
index 0000000000..336dfbf299
--- /dev/null
+++ b/packages/errors/src/serialization/error.test.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { deserializeError, serializeError } from './error';
+
+class CustomError extends Error {
+ readonly customField: any;
+ constructor(message: string) {
+ super(message);
+ this.name = 'CustomError';
+ this.customField = { complex: 'data' };
+ }
+}
+
+describe('serialization', () => {
+ it('runs the happy path with stack', () => {
+ const before = new CustomError('m');
+ const after = deserializeError(
+ serializeError(before, { includeStack: true }),
+ );
+ expect(after.message).toEqual(before.message);
+ expect(after.name).toEqual(before.name);
+ expect(after.stack).toEqual(before.stack);
+ expect(after.customField).toEqual(before.customField);
+ // Does not reconstruct the actual class!
+ expect(after.constructor).not.toBe(before.constructor);
+ });
+
+ it('runs the happy path without stack', () => {
+ const before = new CustomError('m');
+ const after = deserializeError(
+ serializeError(before, { includeStack: false }),
+ );
+ expect(after.message).toEqual(before.message);
+ expect(after.name).toEqual(before.name);
+ expect(after.stack).not.toEqual(before.stack);
+ expect(after.customField).toEqual(before.customField);
+ // Does not reconstruct the actual class!
+ expect(after.constructor).not.toBe(before.constructor);
+ });
+
+ it('serializes stack traces only when allowed', () => {
+ const before = new CustomError('m');
+ const withStack = serializeError(before, { includeStack: true });
+ const withoutStack1 = serializeError(before, { includeStack: false });
+ const withoutStack2 = serializeError(before);
+ expect(withStack.stack).toEqual(before.stack);
+ expect(withoutStack1.stack).not.toBeDefined();
+ expect(withoutStack2.stack).not.toBeDefined();
+ });
+});
diff --git a/packages/errors/src/serialization/error.ts b/packages/errors/src/serialization/error.ts
new file mode 100644
index 0000000000..15a21cee5e
--- /dev/null
+++ b/packages/errors/src/serialization/error.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { JsonObject } from '@backstage/config';
+import {
+ deserializeError as deserializeErrorInternal,
+ serializeError as serializeErrorInternal,
+} from 'serialize-error';
+
+/**
+ * The serialized form of an Error.
+ */
+export type SerializedError = JsonObject & {
+ /** The name of the exception that was thrown */
+ name: string;
+ /** The message of the exception that was thrown */
+ message: string;
+ /** A stringified stack trace; may not be present */
+ stack?: string;
+ /** A custom code (not necessarily the same as an HTTP response code); may not be present */
+ code?: string;
+};
+
+/**
+ * Serializes an error object to a JSON friendly form.
+ *
+ * @param error The error
+ * @param options.includeStackTraces: Include stack trace in the output (default false)
+ */
+export function serializeError(
+ error: Error,
+ options?: { includeStack?: boolean },
+): SerializedError {
+ const serialized = serializeErrorInternal(error);
+ const result: SerializedError = {
+ name: 'Unknown',
+ message: '',
+ ...serialized,
+ };
+
+ if (!options?.includeStack) {
+ delete result.stack;
+ }
+
+ return result;
+}
+
+/**
+ * Deserializes a serialized error object back to an Error.
+ */
+export function deserializeError(
+ data: SerializedError,
+): T {
+ const result = deserializeErrorInternal(data) as T;
+ if (!data.stack) {
+ result.stack = undefined;
+ }
+ return result;
+}
diff --git a/packages/errors/src/serialization/index.ts b/packages/errors/src/serialization/index.ts
new file mode 100644
index 0000000000..18b050d4ac
--- /dev/null
+++ b/packages/errors/src/serialization/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { deserializeError, serializeError } from './error';
+export type { SerializedError } from './error';
+export { parseErrorResponse } from './response';
+export type { ErrorResponse } from './response';
diff --git a/packages/errors/src/serialization/response.test.ts b/packages/errors/src/serialization/response.test.ts
new file mode 100644
index 0000000000..5e0050cba3
--- /dev/null
+++ b/packages/errors/src/serialization/response.test.ts
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { parseErrorResponse, ErrorResponse } from './response';
+
+describe('parseErrorResponse', () => {
+ it('handles the happy path', async () => {
+ const body: ErrorResponse = {
+ error: { name: 'Fours', message: 'Expected fives', stack: 'lines' },
+ request: { method: 'GET', url: '/' },
+ response: { statusCode: 444 },
+ };
+
+ const response: Partial = {
+ status: 444,
+ statusText: 'Fours',
+ text: async () => JSON.stringify(body),
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ };
+
+ await expect(parseErrorResponse(response as Response)).resolves.toEqual(
+ body,
+ );
+ });
+
+ it('uses request header and text body when wrong content type, even if parsable', async () => {
+ const body: ErrorResponse = {
+ error: { name: 'Threes', message: 'Expected twos' },
+ request: { method: 'GET', url: '/' },
+ response: { statusCode: 333 },
+ };
+
+ const response: Partial = {
+ status: 444,
+ statusText: 'Fours',
+ text: async () => JSON.stringify(body),
+ headers: new Headers({ 'Content-Type': 'not-application/not-json' }),
+ };
+
+ await expect(parseErrorResponse(response as Response)).resolves.toEqual({
+ error: {
+ name: 'Unknown',
+ message: `Request failed with status 444 Fours, ${JSON.stringify(
+ body,
+ )}`,
+ },
+ response: { statusCode: 444 },
+ });
+ });
+
+ it('uses request header and text body when not parsable', async () => {
+ const body: ErrorResponse = {
+ error: { name: 'Threes', message: 'Expected twos' },
+ request: { method: 'GET', url: '/' },
+ response: { statusCode: 333 },
+ };
+
+ const response: Partial = {
+ status: 444,
+ statusText: 'Fours',
+ text: async () => JSON.stringify(body).substring(1),
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ };
+
+ await expect(parseErrorResponse(response as Response)).resolves.toEqual({
+ error: {
+ name: 'Unknown',
+ message: `Request failed with status 444 Fours, ${JSON.stringify(
+ body,
+ ).substring(1)}`,
+ },
+ response: { statusCode: 444 },
+ });
+ });
+
+ it('uses request header when failing to get body', async () => {
+ const response: Partial = {
+ status: 444,
+ statusText: 'Fours',
+ text: async () => {
+ throw new Error('bail');
+ },
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ };
+
+ await expect(parseErrorResponse(response as Response)).resolves.toEqual({
+ error: {
+ name: 'Unknown',
+ message: `Request failed with status 444 Fours`,
+ },
+ response: { statusCode: 444 },
+ });
+ });
+});
diff --git a/packages/errors/src/serialization/response.ts b/packages/errors/src/serialization/response.ts
new file mode 100644
index 0000000000..becc84a397
--- /dev/null
+++ b/packages/errors/src/serialization/response.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SerializedError } from './error';
+
+/**
+ * A standard shape of JSON data returned as the body of backend errors.
+ */
+export type ErrorResponse = {
+ /** Details of the error that was caught */
+ error: SerializedError;
+
+ /** Details about the incoming request */
+ request?: {
+ /** The HTTP method of the request */
+ method: string;
+ /** The URL of the request (excluding protocol and host/port) */
+ url: string;
+ };
+
+ /** Details about the response */
+ response: {
+ /** The numeric HTTP status code that was returned */
+ statusCode: number;
+ };
+};
+
+/**
+ * Attempts to construct an ErrorResponse out of a failed server request.
+ * Assumes that the response has already been checked to be not ok. This
+ * function consumes the body of the response, and assumes that it hasn't
+ * been consumed before.
+ *
+ * The code is forgiving, and constructs a useful synthetic body as best it can
+ * if the response body wasn't on the expected form.
+ *
+ * @param response The response of a failed request
+ */
+export async function parseErrorResponse(
+ response: Response,
+): Promise {
+ try {
+ const text = await response.text();
+ if (text) {
+ if (
+ response.headers.get('content-type')?.startsWith('application/json')
+ ) {
+ try {
+ const body = JSON.parse(text);
+ if (body.error && body.response) {
+ return body;
+ }
+ } catch {
+ // ignore
+ }
+ }
+
+ return {
+ error: {
+ name: 'Unknown',
+ message: `Request failed with status ${response.status} ${response.statusText}, ${text}`,
+ },
+ response: {
+ statusCode: response.status,
+ },
+ };
+ }
+ } catch {
+ // ignore
+ }
+
+ return {
+ error: {
+ name: 'Unknown',
+ message: `Request failed with status ${response.status} ${response.statusText}`,
+ },
+ response: {
+ statusCode: response.status,
+ },
+ };
+}
diff --git a/packages/errors/src/setupTests.ts b/packages/errors/src/setupTests.ts
new file mode 100644
index 0000000000..ba33cf996b
--- /dev/null
+++ b/packages/errors/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json
index ed90ea50a6..bf17cc3d9d 100644
--- a/packages/techdocs-common/package.json
+++ b/packages/techdocs-common/package.json
@@ -41,6 +41,7 @@
"@backstage/backend-common": "^0.5.6",
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.3",
+ "@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@google-cloud/storage": "^5.6.0",
"@types/dockerode": "^3.2.1",
diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts
index 856624c568..08a5945e69 100644
--- a/packages/techdocs-common/src/helpers.ts
+++ b/packages/techdocs-common/src/helpers.ts
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-import { Git, InputError, UrlReader } from '@backstage/backend-common';
+import { Git, UrlReader } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { Entity, parseLocationReference } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import fs from 'fs-extra';
diff --git a/packages/techdocs-common/src/stages/prepare/commonGit.ts b/packages/techdocs-common/src/stages/prepare/commonGit.ts
index c38f5ced9d..fecb6cdf0d 100644
--- a/packages/techdocs-common/src/stages/prepare/commonGit.ts
+++ b/packages/techdocs-common/src/stages/prepare/commonGit.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { NotModifiedError } from '@backstage/backend-common';
+import { NotModifiedError } from '@backstage/errors';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import parseGitUrl from 'git-url-parse';
diff --git a/packages/techdocs-common/src/stages/prepare/dir.ts b/packages/techdocs-common/src/stages/prepare/dir.ts
index 08832938df..2f62d88a1c 100644
--- a/packages/techdocs-common/src/stages/prepare/dir.ts
+++ b/packages/techdocs-common/src/stages/prepare/dir.ts
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {
- InputError,
- NotModifiedError,
- UrlReader,
-} from '@backstage/backend-common';
+
+import { UrlReader } from '@backstage/backend-common';
+import { InputError, NotModifiedError } from '@backstage/errors';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import parseGitUrl from 'git-url-parse';
diff --git a/packages/techdocs-common/src/stages/prepare/url.ts b/packages/techdocs-common/src/stages/prepare/url.ts
index 9e4715a4ee..8e463698fb 100644
--- a/packages/techdocs-common/src/stages/prepare/url.ts
+++ b/packages/techdocs-common/src/stages/prepare/url.ts
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { NotModifiedError, UrlReader } from '@backstage/backend-common';
+
+import { NotModifiedError } from '@backstage/errors';
+import { UrlReader } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { Logger } from 'winston';
import { getDocFilesFromRepository } from '../../helpers';
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 5528603529..f1c334cf75 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -33,6 +33,7 @@
"@backstage/catalog-client": "^0.3.7",
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.3",
+ "@backstage/errors": "^0.1.1",
"@backstage/test-utils": "^0.1.8",
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
index e1a3553394..abd3624df2 100644
--- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
+++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ConflictError, NotFoundError } from '@backstage/backend-common';
+import { ConflictError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import { UserEntity } from '@backstage/catalog-model';
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
index d6454fd807..6cf6a94964 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
@@ -22,7 +22,7 @@ import {
BackstageIdentity,
AuthProviderConfig,
} from '../../providers/types';
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { TokenIssuer } from '../../identity';
import { verifyNonce } from './helpers';
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts
index d22fc52499..9998a5b10c 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthEnvironmentHandler.ts
@@ -16,7 +16,7 @@
import express from 'express';
import { Config } from '@backstage/config';
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { readState } from './helpers';
import { AuthProviderRouteHandlers } from '../../providers/types';
diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts
index 85d60661e9..afc58ecbf3 100644
--- a/plugins/auth-backend/src/service/router.ts
+++ b/plugins/auth-backend/src/service/router.ts
@@ -23,10 +23,10 @@ import {
AuthProviderFactory,
} from '../providers';
import {
- NotFoundError,
PluginDatabaseManager,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
+import { NotFoundError } from '@backstage/errors';
import { CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity';
diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json
index 0d0aa5057e..204c6f4002 100644
--- a/plugins/catalog-backend/package.json
+++ b/plugins/catalog-backend/package.json
@@ -33,6 +33,7 @@
"@backstage/backend-common": "^0.5.6",
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.3",
+ "@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@octokit/graphql": "^4.5.8",
"@types/express": "^4.17.6",
diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts
index b03183cf34..7cc88b9f18 100644
--- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts
+++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ConflictError, NotFoundError } from '@backstage/backend-common';
+import { ConflictError, NotFoundError } from '@backstage/errors';
import {
Entity,
entityHasChanges,
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts
index b319bb832d..0e26384358 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ConflictError } from '@backstage/backend-common';
+import { ConflictError } from '@backstage/errors';
import { Entity, Location, parseEntityRef } from '@backstage/catalog-model';
import { EntityFilters } from '../service/EntityFilters';
import { DatabaseManager } from './DatabaseManager';
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index cc83350019..890ce48e0c 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -14,11 +14,7 @@
* limitations under the License.
*/
-import {
- ConflictError,
- InputError,
- NotFoundError,
-} from '@backstage/backend-common';
+import { ConflictError, InputError, NotFoundError } from '@backstage/errors';
import {
Entity,
EntityName,
diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts
index be88aa2810..a7b00c2a4f 100644
--- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts
+++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-import { NotAllowedError, UrlReader } from '@backstage/backend-common';
+import { NotAllowedError } from '@backstage/errors';
+import { UrlReader } from '@backstage/backend-common';
import {
Entity,
EntityPolicy,
EntityRelationSpec,
ENTITY_DEFAULT_NAMESPACE,
LocationSpec,
+ stringifyLocationReference,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { Logger } from 'winston';
@@ -103,7 +105,11 @@ export class LocationReaders implements LocationReader {
output.errors.push({
location: item.location,
error: new NotAllowedError(
- `Entity of kind ${item.entity.kind} is not allowed from location ${item.location.type} ${item.location.target}`,
+ `Entity of kind ${
+ item.entity.kind
+ } is not allowed from location ${stringifyLocationReference(
+ item.location,
+ )}`,
),
});
}
@@ -166,14 +172,20 @@ export class LocationReaders implements LocationReader {
return;
}
} catch (e) {
- const message = `Processor ${processor.constructor.name} threw an error while reading location ${item.location.type} ${item.location.target}, ${e}`;
+ const message = `Processor ${
+ processor.constructor.name
+ } threw an error while reading location ${stringifyLocationReference(
+ item.location,
+ )}, ${e}`;
emit(result.generalError(item.location, message));
logger.warn(message);
}
}
}
- const message = `No processor was able to read location ${item.location.type} ${item.location.target}`;
+ const message = `No processor was able to read location ${stringifyLocationReference(
+ item.location,
+ )}`;
emit(result.inputError(item.location, message));
logger.warn(message);
}
@@ -205,7 +217,11 @@ export class LocationReaders implements LocationReader {
originLocation,
);
} catch (e) {
- const message = `Processor ${processor.constructor.name} threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
+ const message = `Processor ${
+ processor.constructor.name
+ } threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
+ item.location,
+ )}, ${e}`;
emit(result.generalError(item.location, e.message));
logger.warn(message);
return undefined;
@@ -216,14 +232,18 @@ export class LocationReaders implements LocationReader {
try {
const next = await this.options.policy.enforce(current);
if (!next) {
- const message = `Policy unexpectedly returned no data while analyzing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}`;
+ const message = `Policy unexpectedly returned no data while analyzing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
+ item.location,
+ )}`;
emit(result.generalError(item.location, message));
logger.warn(message);
return undefined;
}
current = next;
} catch (e) {
- const message = `Policy check failed while analyzing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
+ const message = `Policy check failed while analyzing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
+ item.location,
+ )}, ${e}`;
emit(result.inputError(item.location, e.message));
logger.warn(message);
return undefined;
@@ -238,7 +258,11 @@ export class LocationReaders implements LocationReader {
break;
}
} catch (e) {
- const message = `Processor ${processor.constructor.name} threw an error while validating the entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
+ const message = `Processor ${
+ processor.constructor.name
+ } threw an error while validating the entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
+ item.location,
+ )}, ${e}`;
emit(result.inputError(item.location, message));
logger.warn(message);
return undefined;
@@ -246,7 +270,9 @@ export class LocationReaders implements LocationReader {
}
}
if (!handled) {
- const message = `No processor recognized the entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}`;
+ const message = `No processor recognized the entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
+ item.location,
+ )}`;
emit(result.inputError(item.location, message));
logger.warn(message);
return undefined;
@@ -261,7 +287,11 @@ export class LocationReaders implements LocationReader {
emit,
);
} catch (e) {
- const message = `Processor ${processor.constructor.name} threw an error while postprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
+ const message = `Processor ${
+ processor.constructor.name
+ } threw an error while postprocessing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
+ item.location,
+ )}, ${e}`;
emit(result.generalError(item.location, message));
logger.warn(message);
return undefined;
@@ -279,7 +309,9 @@ export class LocationReaders implements LocationReader {
const { processors, logger } = this.options;
logger.debug(
- `Encountered error at location ${item.location.type} ${item.location.target}, ${item.error}`,
+ `Encountered error at location ${stringifyLocationReference(
+ item.location,
+ )}, ${item.error}`,
);
const validatedEmit: CatalogProcessorEmit = emitResult => {
@@ -295,7 +327,11 @@ export class LocationReaders implements LocationReader {
try {
await processor.handleError(item.error, item.location, validatedEmit);
} catch (e) {
- const message = `Processor ${processor.constructor.name} threw an error while handling another error at ${item.location.type} ${item.location.target}, ${e}`;
+ const message = `Processor ${
+ processor.constructor.name
+ } threw an error while handling another error at ${stringifyLocationReference(
+ item.location,
+ )}, ${e}`;
emit(result.generalError(item.location, message));
logger.warn(message);
}
diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts
index bdea197426..3855335eb9 100644
--- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-import { NotFoundError, UrlReader } from '@backstage/backend-common';
+import { UrlReader } from '@backstage/backend-common';
+import { NotFoundError } from '@backstage/errors';
import {
Entity,
LocationSpec,
diff --git a/plugins/catalog-backend/src/ingestion/processors/results.ts b/plugins/catalog-backend/src/ingestion/processors/results.ts
index 5f92087cec..da2089adc1 100644
--- a/plugins/catalog-backend/src/ingestion/processors/results.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/results.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError, NotFoundError } from '@backstage/backend-common';
+import { InputError, NotFoundError } from '@backstage/errors';
import {
Entity,
EntityRelationSpec,
diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts b/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts
index 53201486bc..ab6e287698 100644
--- a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts
+++ b/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts
@@ -116,7 +116,7 @@ describe('parseEntityYaml', () => {
});
it('should handle empty yaml documents', () => {
- // This happens if the user accidentially adds a "---"
+ // This happens if the user accidentally adds a "---"
// at the end of a file
const results = Array.from(
parseEntityYaml(
@@ -205,7 +205,7 @@ describe('parseEntityYaml', () => {
it('must be an object at root', () => {
const results = Array.from(
- parseEntityYaml(Buffer.from('imma-string', 'utf8'), testLoc),
+ parseEntityYaml(Buffer.from('i-am-a-string', 'utf8'), testLoc),
);
expect(results).toEqual([
diff --git a/plugins/catalog-backend/src/service/EntityFilters.ts b/plugins/catalog-backend/src/service/EntityFilters.ts
index 5a93407424..83c0bfea6c 100644
--- a/plugins/catalog-backend/src/service/EntityFilters.ts
+++ b/plugins/catalog-backend/src/service/EntityFilters.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { EntitiesSearchFilter, EntityFilter } from '../database';
/**
diff --git a/plugins/catalog-backend/src/service/filterQuery.ts b/plugins/catalog-backend/src/service/filterQuery.ts
index e9b6ef6062..5e6b18ddf1 100644
--- a/plugins/catalog-backend/src/service/filterQuery.ts
+++ b/plugins/catalog-backend/src/service/filterQuery.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { Entity } from '@backstage/catalog-model';
import lodash from 'lodash';
import { RecursivePartial } from '../util';
diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts
index cef40a9aac..f7655b859e 100644
--- a/plugins/catalog-backend/src/service/router.test.ts
+++ b/plugins/catalog-backend/src/service/router.test.ts
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-import { getVoidLogger, NotFoundError } from '@backstage/backend-common';
+import { getVoidLogger } from '@backstage/backend-common';
+import { NotFoundError } from '@backstage/errors';
import type { Entity, LocationSpec } from '@backstage/catalog-model';
import express from 'express';
import request from 'supertest';
diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts
index 5936e3a747..96e424c70b 100644
--- a/plugins/catalog-backend/src/service/router.ts
+++ b/plugins/catalog-backend/src/service/router.ts
@@ -14,20 +14,21 @@
* limitations under the License.
*/
-import { errorHandler, NotFoundError } from '@backstage/backend-common';
-import {
- locationSpecSchema,
- analyzeLocationSchema,
-} from '@backstage/catalog-model';
+import { errorHandler } from '@backstage/backend-common';
import type { Entity } from '@backstage/catalog-model';
+import {
+ analyzeLocationSchema,
+ locationSpecSchema,
+} from '@backstage/catalog-model';
+import { NotFoundError } from '@backstage/errors';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import yn from 'yn';
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
-import { LocationAnalyzer, HigherOrderOperation } from '../ingestion/types';
-import { translateQueryToFieldMapper } from './filterQuery';
+import { HigherOrderOperation, LocationAnalyzer } from '../ingestion/types';
import { EntityFilters } from './EntityFilters';
+import { translateQueryToFieldMapper } from './filterQuery';
import { requireRequestBody, validateRequestBody } from './util';
export interface RouterOptions {
@@ -105,7 +106,7 @@ export async function createRouter(
);
if (!entities.length) {
throw new NotFoundError(
- `No entity with kind ${kind} namespace ${namespace} name ${name}`,
+ `No entity named '${name}' found, with kind '${kind}' in namespace '${namespace}'`,
);
}
res.status(200).json(entities[0]);
diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts
index 39692030e3..d806f630e8 100644
--- a/plugins/catalog-backend/src/service/util.ts
+++ b/plugins/catalog-backend/src/service/util.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { Request } from 'express';
import lodash from 'lodash';
import yup from 'yup';
diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx
index 6f9899ff82..51c8f85116 100644
--- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx
+++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx
@@ -25,6 +25,7 @@ import {
Link,
Page,
Progress,
+ ResponseErrorPanel,
WarningPanel,
} from '@backstage/core';
import {
@@ -34,7 +35,6 @@ import {
useEntityCompoundName,
} from '@backstage/plugin-catalog-react';
import { Box } from '@material-ui/core';
-import { Alert } from '@material-ui/lab';
import React, { PropsWithChildren, useContext, useState } from 'react';
import { useNavigate } from 'react-router';
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
@@ -143,7 +143,7 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
{error && (
- {error.toString()}
+
)}
diff --git a/plugins/graphiql/src/index.ts b/plugins/graphiql/src/index.ts
index ed64779eba..10149e37a2 100644
--- a/plugins/graphiql/src/index.ts
+++ b/plugins/graphiql/src/index.ts
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+import { IconComponent } from '@backstage/core';
+import GraphiQLIconComponent from './assets/graphiql.icon.svg';
+
export {
graphiqlPlugin,
graphiqlPlugin as plugin,
@@ -22,3 +25,4 @@ export {
export { GraphiQLPage as Router } from './components';
export * from './lib/api';
export * from './route-refs';
+export const GraphiQLIcon: IconComponent = GraphiQLIconComponent;
diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json
index f4aa5cf1ce..9a23c588db 100644
--- a/plugins/scaffolder-backend/package.json
+++ b/plugins/scaffolder-backend/package.json
@@ -33,6 +33,7 @@
"@backstage/catalog-client": "^0.3.7",
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.3",
+ "@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.5.1",
"@gitbeaker/core": "^28.0.2",
"@gitbeaker/node": "^28.0.2",
diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts
index 46a1433be2..806f3ecad5 100644
--- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts
+++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts
@@ -19,7 +19,7 @@ import {
TemplateEntityV1beta2,
} from '@backstage/catalog-model';
import { CatalogApi } from '@backstage/catalog-client';
-import { ConflictError, NotFoundError } from '@backstage/backend-common';
+import { ConflictError, NotFoundError } from '@backstage/errors';
/**
* A catalog client tailored for reading out entity data from the catalog.
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts
index c9e163bfdd..4f8042336c 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/TemplateActionRegistry.ts
@@ -15,7 +15,7 @@
*/
import { InputBase, TemplateAction } from './types';
-import { ConflictError, NotFoundError } from '@backstage/backend-common';
+import { ConflictError, NotFoundError } from '@backstage/errors';
export class TemplateActionRegistry {
private readonly actions = new Map>();
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts
index b034b480e2..8ee98f5d20 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { CatalogApi } from '@backstage/catalog-client';
import { getEntityName } from '@backstage/catalog-model';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts
index 070aaa9679..814146cc50 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/cookiecutter.ts
@@ -17,7 +17,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import Docker from 'dockerode';
-import { InputError, UrlReader } from '@backstage/backend-common';
+import { UrlReader } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { JsonObject } from '@backstage/config';
import { TemplaterBuilder, TemplaterValues } from '../../../stages/templater';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts
index 652a1a56ca..67fe9533aa 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts
@@ -16,7 +16,8 @@
import fs from 'fs-extra';
import { resolve as resolvePath, isAbsolute } from 'path';
-import { InputError, UrlReader } from '@backstage/backend-common';
+import { UrlReader } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { JsonValue } from '@backstage/config';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts
index 00c63535ca..08316e95e6 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts
@@ -15,7 +15,8 @@
*/
import path from 'path';
-import { InputError, UrlReader } from '@backstage/backend-common';
+import { UrlReader } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import { fetchContents } from './helpers';
import { createTemplateAction } from '../../createTemplateAction';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts
index db38960086..0141de3499 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { initRepoAndPush } from '../../../stages/publish/helpers';
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts
index 03881af35d..a7370356cc 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import {
BitbucketIntegrationConfig,
ScmIntegrationRegistry,
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts
index cb11e3d08a..4f9b6038a1 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import {
GithubCredentialsProvider,
ScmIntegrationRegistry,
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts
index eefdcdf11b..12fc90cc94 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { Gitlab } from '@gitbeaker/node';
import { initRepoAndPush } from '../../../stages/publish/helpers';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts
index 9349d5b5eb..89bbf24574 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
export const parseRepoUrl = (repoUrl: string) => {
let parsed;
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts
index 924b9f5811..b4e13e3ad4 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/helpers.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import {
LOCATION_ANNOTATION,
parseLocationReference,
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
index 687cc39452..9eb380be13 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { PreparerBase, PreparerOptions } from './types';
export class FilePreparer implements PreparerBase {
diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts
index cb1e8532f2..f5080907df 100644
--- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/helpers.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { spawn } from 'child_process';
import { PassThrough, Writable } from 'stream';
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts
index f3d7766d86..91264bd3e2 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts
@@ -15,11 +15,8 @@
*/
import { JsonObject } from '@backstage/config';
-import {
- ConflictError,
- NotFoundError,
- resolvePackagePath,
-} from '@backstage/backend-common';
+import { resolvePackagePath } from '@backstage/backend-common';
+import { ConflictError, NotFoundError } from '@backstage/errors';
import { Knex } from 'knex';
import { v4 as uuid } from 'uuid';
import {
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
index bd0a418598..3a846f79a5 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
@@ -24,7 +24,7 @@ import fs from 'fs-extra';
import path from 'path';
import { TemplateActionRegistry } from '../actions/TemplateActionRegistry';
import * as handlebars from 'handlebars';
-import { InputError } from '@backstage/backend-common';
+import { InputError } from '@backstage/errors';
type Options = {
logger: Logger;
diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts
index 81be03730b..ec7e79bbc8 100644
--- a/plugins/scaffolder-backend/src/service/router.ts
+++ b/plugins/scaffolder-backend/src/service/router.ts
@@ -42,12 +42,8 @@ import { templateEntityToSpec } from '../scaffolder/tasks/TemplateConverter';
import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry';
import { createLegacyActions } from '../scaffolder/stages/legacy';
import { getEntityBaseUrl, getWorkingDirectory } from './helpers';
-import {
- InputError,
- NotFoundError,
- PluginDatabaseManager,
- UrlReader,
-} from '@backstage/backend-common';
+import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common';
+import { InputError, NotFoundError } from '@backstage/errors';
import { CatalogApi } from '@backstage/catalog-client';
import {
TemplateEntityV1alpha1,
diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json
index f3a87ff569..4e716dab32 100644
--- a/plugins/techdocs-backend/package.json
+++ b/plugins/techdocs-backend/package.json
@@ -33,6 +33,7 @@
"@backstage/backend-common": "^0.5.6",
"@backstage/catalog-model": "^0.7.4",
"@backstage/config": "^0.1.3",
+ "@backstage/errors": "^0.1.1",
"@backstage/techdocs-common": "^0.4.4",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts
index 50577ab130..f42a708bb2 100644
--- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts
+++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { NotModifiedError } from '@backstage/backend-common';
+import { NotModifiedError } from '@backstage/errors';
import { Entity, serializeEntityRef } from '@backstage/catalog-model';
import {
GeneratorBase,
diff --git a/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx b/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx
index 072c90ddac..e299b30594 100644
--- a/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx
+++ b/plugins/user-settings/src/components/AuthProviders/AuthProviders.test.tsx
@@ -64,7 +64,9 @@ describe('', () => {
expect(rendered.getByText('Google')).toBeInTheDocument();
expect(
- rendered.getByText(googleAuthApiRef.description),
+ rendered.getByText(
+ 'Provides authentication towards Google APIs and identities',
+ ),
).toBeInTheDocument();
const button = rendered.getByTitle('Sign in to Google');
diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx
index 415d1eaad6..b9c7b763b6 100644
--- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx
+++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx
@@ -34,7 +34,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
{configuredProviders.includes('google') && (
@@ -42,7 +42,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
{configuredProviders.includes('microsoft') && (
@@ -50,7 +50,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
{configuredProviders.includes('github') && (
@@ -58,7 +58,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
{configuredProviders.includes('gitlab') && (
@@ -66,7 +66,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
{configuredProviders.includes('okta') && (
@@ -74,7 +74,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => (
{configuredProviders.includes('oauth2') && (
diff --git a/yarn.lock b/yarn.lock
index bcdb5e3861..c549bc86db 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1772,6 +1772,7 @@
dependencies:
"@backstage/config" "^0.1.3"
"@backstage/core-api" "^0.2.13"
+ "@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.4"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
@@ -1813,6 +1814,7 @@
dependencies:
"@backstage/config" "^0.1.3"
"@backstage/core-api" "^0.2.13"
+ "@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.4"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
@@ -1854,6 +1856,7 @@
dependencies:
"@backstage/config" "^0.1.3"
"@backstage/core-api" "^0.2.13"
+ "@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.4"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
@@ -23437,6 +23440,13 @@ serialize-error@^2.1.0:
resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a"
integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=
+serialize-error@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-8.0.1.tgz#7a67f8ecbbf28973b5a954a2852ff9f4eef52d99"
+ integrity sha512-r5o60rWFS+8/b49DNAbB+GXZA0SpDpuWE758JxDKgRTga05r3U5lwyksE91dYKDhXSmnu36RALj615E6Aj5pSg==
+ dependencies:
+ type-fest "^0.20.2"
+
serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
@@ -25513,6 +25523,11 @@ type-fest@^0.18.0:
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
+type-fest@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+ integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
type-fest@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8"