backend-app-api: filter internal errors

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-19 13:07:43 +01:00
parent 25adbdde2d
commit 7ae5704903
4 changed files with 100 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Updated the default error handling middleware to filter out certain known error types that should never be returned in responses. The errors are instead logged along with a correlation ID, which is also returned in the response. Initially only PostgreSQL protocol errors from the `pg-protocol` package are filtered out.
@@ -192,6 +192,36 @@ describe('MiddlewareFactory', () => {
);
});
it('should filter out internal errors', async () => {
const app = express();
class DatabaseError extends Error {}
const thrownError = new DatabaseError('some error');
app.use('/breaks', () => {
throw thrownError;
});
app.use(middleware.error());
await request(app).get('/breaks');
expect(childLogger.error).toHaveBeenCalledTimes(2);
expect(childLogger.error).toHaveBeenCalledWith(
'Request failed with status 500',
expect.objectContaining({
message: expect.stringMatching(
/^An internal error occurred logId=[0-9a-f]+$/,
),
}),
);
expect(childLogger.error).toHaveBeenCalledWith(
expect.stringMatching(
/^Filtered internal error with logId=[0-9a-f]+ from response$/,
),
thrownError,
);
});
it('does not log 400 errors', async () => {
const app = express();
@@ -43,6 +43,7 @@ import {
serializeError,
} from '@backstage/errors';
import { NotImplementedError } from '@backstage/errors';
import { applyInternalErrorFilter } from './applyInternalErrorFilter';
/**
* Options used to create a {@link MiddlewareFactory}.
@@ -209,7 +210,14 @@ export class MiddlewareFactory {
type: 'errorHandler',
});
return (error: Error, req: Request, res: Response, next: NextFunction) => {
return (
rawError: Error,
req: Request,
res: Response,
next: NextFunction,
) => {
const error = applyInternalErrorFilter(rawError, logger);
const statusCode = getStatusCode(error);
if (options.logAllErrors || statusCode >= 500) {
logger.error(`Request failed with status ${statusCode}`, error);
@@ -0,0 +1,56 @@
/*
* Copyright 2024 The Backstage Authors
*
* 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 { LoggerService } from '@backstage/backend-plugin-api';
import { assertError } from '@backstage/errors';
import { randomBytes } from 'crypto';
function handleBadError(error: Error, logger: LoggerService) {
const logId = randomBytes(10).toString('hex');
logger.error(
`Filtered internal error with logId=${logId} from response`,
error,
);
const newError = new Error(`An internal error occurred logId=${logId}`);
delete newError.stack; // Trim the stack since it's not particularly useful
return newError;
}
/**
* Filters out certain known error types that should never be returned in responses.
*
* @internal
*/
export function applyInternalErrorFilter(
error: unknown,
logger: LoggerService,
): Error {
try {
assertError(error);
} catch (assertionError: unknown) {
assertError(assertionError);
return handleBadError(assertionError, logger);
}
const constructorName = error.constructor.name;
// DatabaseError are thrown by the pg-protocol module
if (constructorName === 'DatabaseError') {
return handleBadError(error, logger);
}
return error;
}