fix review comments

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-03-10 13:48:43 +01:00
parent 8686eb38cf
commit fda249ff6f
28 changed files with 511 additions and 296 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/catalog-client': patch
---
Throw the new `ServerResponseError` from `@backstage/errors`
Throw the new `ResponseError` from `@backstage/errors`
+3 -1
View File
@@ -16,7 +16,6 @@ After:
```json
{
"error": {
"statusCode": 404,
"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)"
@@ -24,6 +23,9 @@ After:
"request": {
"method": "GET",
"url": "/entities/by-name/user/default/tara.macgovern2"
},
"response": {
"statusCode": 404
}
}
```
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/core': patch
---
Add a `ErrorResponsePanel` to render `ServerResponseError` from `@backstage/errors`
Add a `ResponseErrorPanel` to render `ResponseError` from `@backstage/errors`
@@ -39,15 +39,12 @@ describe('errorHandler', () => {
expect(response.status).toBe(500);
expect(response.body).toEqual({
error: {
statusCode: 500,
error: expect.objectContaining({
name: 'Error',
message: 'some message',
},
request: {
method: 'GET',
url: '/breaks',
},
}),
request: { method: 'GET', url: '/breaks' },
response: { statusCode: 500 },
});
});
@@ -86,14 +83,17 @@ describe('errorHandler', () => {
expect(response.status).toBe(432);
expect(response.body).toEqual({
error: {
statusCode: 432,
expose: true,
name: 'BadRequestError',
message: 'Some Message',
status: 432,
statusCode: 432,
},
request: {
method: 'GET',
url: '/breaks',
},
response: { statusCode: 432 },
});
});
@@ -14,17 +14,18 @@
* limitations under the License.
*/
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
import { Logger } from 'winston';
import {
NotModifiedError,
InputError,
AuthenticationError,
ConflictError,
ErrorResponse,
InputError,
NotAllowedError,
NotFoundError,
ConflictError,
ServerResponseErrorBody,
NotModifiedError,
serializeError,
} from '@backstage/errors';
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
import { Logger } from 'winston';
import { getRootLogger } from '../logging';
export type ErrorHandlerOptions = {
@@ -86,17 +87,10 @@ export function errorHandler(
return;
}
const body: ServerResponseErrorBody = {
error: {
statusCode,
name: error.name || 'Error',
message: error.message || '<no reason given>',
stack: showStackTraces ? error.stack : undefined,
},
request: {
method: req.method,
url: req.url,
},
const body: ErrorResponse = {
error: serializeError(error, { includeStack: showStackTraces }),
request: { method: req.method, url: req.url },
response: { statusCode },
};
res.status(statusCode).json(body);
+4 -4
View File
@@ -21,7 +21,7 @@ import {
LOCATION_ANNOTATION,
stringifyLocationReference,
} from '@backstage/catalog-model';
import { ServerResponseError } from '@backstage/errors';
import { ResponseError } from '@backstage/errors';
import fetch from 'cross-fetch';
import {
AddLocationRequest,
@@ -154,7 +154,7 @@ export class CatalogClient implements CatalogApi {
},
);
if (!response.ok) {
throw await ServerResponseError.forResponse(response);
throw await ResponseError.fromResponse(response);
}
return undefined;
}
@@ -175,7 +175,7 @@ export class CatalogClient implements CatalogApi {
});
if (!response.ok) {
throw await ServerResponseError.forResponse(response);
throw await ResponseError.fromResponse(response);
}
return await response.json();
@@ -196,7 +196,7 @@ export class CatalogClient implements CatalogApi {
if (response.status === 404) {
return undefined;
}
throw await ServerResponseError.forResponse(response);
throw await ResponseError.fromResponse(response);
}
return await response.json();
@@ -1,135 +0,0 @@
/*
* 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 { ServerResponseError } 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 ServerResponseError errors, to display rich
* server-provided information about what happened.
*/
export const ErrorResponse = ({ error }: Props) => {
const classes = useStyles();
if (error.name !== 'ServerResponseError') {
return (
<List>
<ListItemText primary="Message" secondary={error.message} />
</List>
);
}
const { body } = error as ServerResponseError;
const errorString = `${body.error.statusCode}: ${body.error.name}`;
const request = body.request && `${body.request.method} ${body.request.url}`;
const message = body.error.message.replace(/\\n/g, '\n');
const stack = body.error.stack?.replace(/\\n/g, '\n');
const json = JSON.stringify(error, undefined, 2);
return (
<>
<List dense>
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Error"
secondary={errorString}
/>
<CopyTextButton text={errorString} />
</ListItem>
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Message"
secondary={message}
/>
<CopyTextButton text={message} />
</ListItem>
{request ? (
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Request"
secondary={request}
/>
<CopyTextButton text={request} />
</ListItem>
) : null}
{stack ? (
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Stack Trace"
secondary={stack}
/>
<CopyTextButton text={stack} />
</ListItem>
) : null}
<Divider component="li" className={classes.divider} />
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Full Error as JSON"
secondary={<CodeSnippet language="json" text={json} />}
/>
<CopyTextButton text={json} />
</ListItem>
</List>
</>
);
};
/**
* Renders a warning panel as the effect of a failed server request.
*
* Has special treatment for ServerResponseError errors, to display rich
* server-provided information about what happened.
*/
export const ErrorResponsePanel = ({ error }: Props) => {
return (
<WarningPanel title={error.message}>
<ErrorResponse error={error} />
</WarningPanel>
);
};
@@ -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 (
<List dense>
<ListItemText primary="Message" secondary={error.message} />
</List>
);
}
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 (
<List dense>
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Error"
secondary={errorString}
/>
<CopyTextButton text={errorString} />
</ListItem>
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Message"
secondary={messageString}
/>
<CopyTextButton text={messageString} />
</ListItem>
{requestString && (
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Request"
secondary={requestString}
/>
<CopyTextButton text={requestString} />
</ListItem>
)}
{stackString && (
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Stack Trace"
secondary={stackString}
/>
<CopyTextButton text={stackString} />
</ListItem>
)}
<Divider component="li" className={classes.divider} />
<ListItem alignItems="flex-start">
<ListItemText
classes={{ secondary: classes.text }}
primary="Full Error as JSON"
secondary={<CodeSnippet language="json" text={jsonString} />}
/>
<CopyTextButton text={jsonString} />
</ListItem>
</List>
);
};
/**
* 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 (
<WarningPanel title={error.message}>
<ResponseErrorDetails error={error} />
</WarningPanel>
);
};
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { ErrorResponse, ErrorResponsePanel } from './ErrorResponse';
export { ResponseErrorDetails, ResponseErrorPanel } from './ResponseErrorPanel';
+1 -1
View File
@@ -22,7 +22,7 @@ export * from './CopyTextButton';
export * from './DependencyGraph';
export * from './DismissableBanner';
export * from './EmptyState';
export * from './ErrorResponse';
export * from './ResponseErrorPanel';
export * from './FeatureDiscovery';
export * from './HeaderIconLinkRow';
export * from './HorizontalScrollGrid';
+2 -1
View File
@@ -30,7 +30,8 @@
},
"dependencies": {
"@backstage/config": "^0.1.2",
"cross-fetch": "^3.0.6"
"cross-fetch": "^3.0.6",
"serialize-error": "^8.0.1"
},
"devDependencies": {
"@backstage/cli": "^0.6.1",
-43
View File
@@ -1,43 +0,0 @@
/*
* 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 {
parseServerResponseErrorBody,
ServerResponseErrorBody,
} from '../payload';
/**
* An error thrown as the result of a failed server request.
*
* The server is expected to respond on the ServerResponseErrorBody format.
*/
export class ServerResponseError extends Error {
static async forResponse(response: Response): Promise<ServerResponseError> {
const body = await parseServerResponseErrorBody(response);
const status = body.error.statusCode || response.status;
const statusText = body.error.name || response.statusText;
const message = `Request failed with ${status} ${statusText}`;
return new ServerResponseError(message, body);
}
readonly body: ServerResponseErrorBody;
constructor(message: string, body: ServerResponseErrorBody) {
super(message);
this.name = 'ServerResponseError';
this.body = body;
}
}
@@ -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<Response> = {
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');
});
});
@@ -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<ResponseError> {
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;
}
}
@@ -15,11 +15,12 @@
*/
export {
InputError,
AuthenticationError,
ConflictError,
InputError,
NotAllowedError,
NotFoundError,
ConflictError,
NotModifiedError,
} from './common';
export { ServerResponseError } from './server';
export { CustomErrorBase } from './CustomErrorBase';
export { ResponseError } from './ResponseError';
+2 -2
View File
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export * from './error';
export * from './payload';
export * from './errors';
export * from './serialization';
@@ -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<CustomError>(
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<CustomError>(
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();
});
});
@@ -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: '<no reason given>',
...serialized,
};
if (!options?.includeStack) {
delete result.stack;
}
return result;
}
/**
* Deserializes a serialized error object back to an Error.
*/
export function deserializeError<T extends Error = Error>(
data: SerializedError,
): T {
const result = deserializeErrorInternal(data) as T;
if (!data.stack) {
result.stack = undefined;
}
return result;
}
@@ -14,5 +14,7 @@
* limitations under the License.
*/
export { parseServerResponseErrorBody } from './ServerResponseErrorBody';
export type { ServerResponseErrorBody } from './ServerResponseErrorBody';
export { deserializeError, serializeError } from './error';
export type { SerializedError } from './error';
export { parseErrorResponse } from './response';
export type { ErrorResponse } from './response';
@@ -14,24 +14,14 @@
* limitations under the License.
*/
import {
parseServerResponseErrorBody,
ServerResponseErrorBody,
} from './ServerResponseErrorBody';
import { parseErrorResponse, ErrorResponse } from './response';
describe('parseServerResponseErrorBody', () => {
describe('parseErrorResponse', () => {
it('handles the happy path', async () => {
const body: ServerResponseErrorBody = {
error: {
statusCode: 444,
name: 'Fours',
message: 'Expected fives',
stack: 'lines',
},
request: {
method: 'GET',
url: '/',
},
const body: ErrorResponse = {
error: { name: 'Fours', message: 'Expected fives', stack: 'lines' },
request: { method: 'GET', url: '/' },
response: { statusCode: 444 },
};
const response: Partial<Response> = {
@@ -41,22 +31,16 @@ describe('parseServerResponseErrorBody', () => {
headers: new Headers({ 'Content-Type': 'application/json' }),
};
await expect(
parseServerResponseErrorBody(response as Response),
).resolves.toEqual(body);
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: ServerResponseErrorBody = {
error: {
statusCode: 333,
name: 'Threes',
message: 'Expected twos',
},
request: {
method: 'GET',
url: '/',
},
const body: ErrorResponse = {
error: { name: 'Threes', message: 'Expected twos' },
request: { method: 'GET', url: '/' },
response: { statusCode: 333 },
};
const response: Partial<Response> = {
@@ -66,30 +50,22 @@ describe('parseServerResponseErrorBody', () => {
headers: new Headers({ 'Content-Type': 'not-application/not-json' }),
};
await expect(
parseServerResponseErrorBody(response as Response),
).resolves.toEqual({
await expect(parseErrorResponse(response as Response)).resolves.toEqual({
error: {
statusCode: 444,
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: ServerResponseErrorBody = {
error: {
statusCode: 333,
name: 'Threes',
message: 'Expected twos',
},
request: {
method: 'GET',
url: '/',
},
const body: ErrorResponse = {
error: { name: 'Threes', message: 'Expected twos' },
request: { method: 'GET', url: '/' },
response: { statusCode: 333 },
};
const response: Partial<Response> = {
@@ -99,16 +75,14 @@ describe('parseServerResponseErrorBody', () => {
headers: new Headers({ 'Content-Type': 'application/json' }),
};
await expect(
parseServerResponseErrorBody(response as Response),
).resolves.toEqual({
await expect(parseErrorResponse(response as Response)).resolves.toEqual({
error: {
statusCode: 444,
name: 'Unknown',
message: `Request failed with status 444 Fours, ${JSON.stringify(
body,
).substring(1)}`,
},
response: { statusCode: 444 },
});
});
@@ -122,14 +96,12 @@ describe('parseServerResponseErrorBody', () => {
headers: new Headers({ 'Content-Type': 'application/json' }),
};
await expect(
parseServerResponseErrorBody(response as Response),
).resolves.toEqual({
await expect(parseErrorResponse(response as Response)).resolves.toEqual({
error: {
statusCode: 444,
name: 'Unknown',
message: `Request failed with status 444 Fours`,
},
response: { statusCode: 444 },
});
});
});
@@ -14,43 +14,44 @@
* limitations under the License.
*/
import { SerializedError } from './error';
/**
* A standard shape of JSON data returned as the body of backend errors.
*/
export type ServerResponseErrorBody = {
export type ErrorResponse = {
/** Details of the error that was caught */
error: {
/** The numeric HTTP status code that was returned */
statusCode: number;
/** 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;
};
error: SerializedError;
/** The incoming request */
/** 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 extract the ServerResponseErrorBody out of a server response.
* This consumes the body of the response.
* 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 wasn't on the expected form.
* if the response body wasn't on the expected form.
*
* @param response The response of a failed request
*/
export async function parseServerResponseErrorBody(
export async function parseErrorResponse(
response: Response,
): Promise<ServerResponseErrorBody> {
): Promise<ErrorResponse> {
try {
const text = await response.text();
if (text) {
@@ -59,7 +60,7 @@ export async function parseServerResponseErrorBody(
) {
try {
const body = JSON.parse(text);
if (body.error && body.request) {
if (body.error && body.response) {
return body;
}
} catch {
@@ -69,10 +70,12 @@ export async function parseServerResponseErrorBody(
return {
error: {
statusCode: response.status,
name: 'Unknown',
message: `Request failed with status ${response.status} ${response.statusText}, ${text}`,
},
response: {
statusCode: response.status,
},
};
}
} catch {
@@ -81,9 +84,11 @@ export async function parseServerResponseErrorBody(
return {
error: {
statusCode: response.status,
name: 'Unknown',
message: `Request failed with status ${response.status} ${response.statusText}`,
},
response: {
statusCode: response.status,
},
};
}
@@ -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([
@@ -15,12 +15,12 @@
*/
import { errorHandler } from '@backstage/backend-common';
import { NotFoundError } from '@backstage/errors';
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';
@@ -20,12 +20,12 @@ import {
} from '@backstage/catalog-model';
import {
Content,
ErrorResponsePanel,
Header,
HeaderLabel,
Link,
Page,
Progress,
ResponseErrorPanel,
WarningPanel,
} from '@backstage/core';
import {
@@ -143,7 +143,7 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
{error && (
<Content>
<ErrorResponsePanel error={error} />
<ResponseErrorPanel error={error} />
</Content>
)}
+15
View File
@@ -1772,6 +1772,7 @@
dependencies:
"@backstage/config" "^0.1.3"
"@backstage/core-api" "^0.2.12"
"@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.3"
"@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.12"
"@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.3"
"@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.12"
"@backstage/errors" "^0.1.1"
"@backstage/theme" "^0.2.3"
"@material-ui/core" "^4.11.0"
"@material-ui/icons" "^4.9.1"
@@ -23380,6 +23383,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"
@@ -25456,6 +25466,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"