From f9213f6f232740c094885154d64323794fe71587 Mon Sep 17 00:00:00 2001 From: Michael Courtin Date: Thu, 30 Dec 2021 16:35:58 +0100 Subject: [PATCH] fix: provide generic exception handling functionality Introduce a log helper with static functions to get exception message / exception stack whatever is thrown or provided. Closes: #1702 Signed-off-by: Michael Courtin --- .../src/main/typescript/api-server.ts | 9 +- .../src/main/typescript/logging/log-helper.ts | 100 ++++++ .../src/main/typescript/public-api.ts | 1 + .../unit/logging/log-helper.test.ts | 301 ++++++++++++++++++ 4 files changed, 408 insertions(+), 3 deletions(-) create mode 100644 packages/cactus-common/src/main/typescript/logging/log-helper.ts create mode 100644 packages/cactus-common/src/test/typescript/unit/logging/log-helper.test.ts diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index 75dcda5cb34..3888c563894 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -43,6 +43,7 @@ import { Bools, Logger, LoggerProvider, + LogHelper, Servers, } from "@hyperledger/cactus-common"; @@ -241,7 +242,8 @@ export class ApiServer { return { addressInfoCockpit, addressInfoApi, addressInfoGrpc }; } catch (ex) { - const errorMessage = `Failed to start ApiServer: ${ex.stack}`; + const stack = LogHelper.getExceptionStack(ex); + const errorMessage = `Failed to start ApiServer: ${stack}`; this.log.error(errorMessage); this.log.error(`Attempting shutdown...`); try { @@ -296,9 +298,10 @@ export class ApiServer { await this.getPluginImportsCount(), ); return this.pluginRegistry; - } catch (e) { + } catch (ex) { this.pluginRegistry = new PluginRegistry({ plugins: [] }); - const errorMessage = `Failed init PluginRegistry: ${e.stack}`; + const stack = LogHelper.getExceptionStack(ex); + const errorMessage = `Failed init PluginRegistry: ${stack}`; this.log.error(errorMessage); throw new Error(errorMessage); } diff --git a/packages/cactus-common/src/main/typescript/logging/log-helper.ts b/packages/cactus-common/src/main/typescript/logging/log-helper.ts new file mode 100644 index 00000000000..5fca30528e4 --- /dev/null +++ b/packages/cactus-common/src/main/typescript/logging/log-helper.ts @@ -0,0 +1,100 @@ +import { RuntimeError } from "run-time-error"; + +export class LogHelper { + public static getExceptionStack(exception: unknown): string { + // handle unknown exception input + const defaultStack = "NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION"; + const invalidStack = "INVALID_STACK_INFORMATION"; + let stack = defaultStack; + let exceptionHandled = false; + + // 1st need to check that exception is not null or undefined before trying to access the wanted stack information + if (exception) { + if (exception instanceof RuntimeError) { + // handling RuntimeError stack inclusive nested / cascaded stacks + stack = this.safeJsonStringify(exception); + exceptionHandled = true; + } + + if (!exceptionHandled && typeof exception === "object") { + // 2nd need to check if a stack property is available + if (Object.hasOwnProperty.call(exception, "stack")) { + // 3rd check if the stack property is already of type string + if ( + typeof (exception as Record).stack === "string" + ) { + stack = (exception as { stack: string }).stack; + } else { + // need to stringify stack information first + stack = this.safeJsonStringify( + (exception as { stack: unknown }).stack, + invalidStack, + ); + } + } + } + } + return stack; + } + + public static getExceptionMessage(exception: unknown): string { + // handle unknown exception input + const defaultMessage = "NO_MESSAGE_INCLUDED_IN_EXCEPTION"; + const invalidException = "INVALID_EXCEPTION"; + const invalidMessage = "INVALID_EXCEPTION_MESSAGE"; + const customExceptionPrefix = "A CUSTOM EXCEPTION WAS THROWN: "; + let message = defaultMessage; + let exceptionHandled = false; + + // 1st need to check that exception is not null or undefined before trying to access the wanted message information + if (exception) { + if (typeof exception === "object") { + // 2nd need to check if a message property is available + if (Object.hasOwnProperty.call(exception, "message")) { + // 3rd check if the message property is already of type string + if ( + typeof (exception as Record).message === "string" + ) { + message = (exception as { message: string }).message; + } else { + // need to stringify message information first + message = this.safeJsonStringify( + (exception as { message: unknown }).message, + invalidMessage, + ); + } + exceptionHandled = true; + } + } + + // handling of custom exceptions + if (!exceptionHandled) { + // check if thrown custom exception is a string type only -> directly use it as exception message + if (typeof exception === "string") { + message = exception; + } else { + // custom exception is of a different type -> need to stringify it + message = + customExceptionPrefix && + this.safeJsonStringify(exception, invalidException); + } + } + } + return message; + } + + private static safeJsonStringify( + input: unknown, + catchMessage = "INVALID_INPUT", + ): string { + let message = ""; + + try { + message = JSON.stringify(input); + } catch (error) { + // stringify failed maybe due to cyclic dependency + message = catchMessage; + } + return message; + } +} diff --git a/packages/cactus-common/src/main/typescript/public-api.ts b/packages/cactus-common/src/main/typescript/public-api.ts index 209b91f6dcc..d46813e557d 100755 --- a/packages/cactus-common/src/main/typescript/public-api.ts +++ b/packages/cactus-common/src/main/typescript/public-api.ts @@ -1,4 +1,5 @@ export { LoggerProvider } from "./logging/logger-provider"; +export { LogHelper } from "./logging/log-helper"; export { Logger, ILoggerOptions } from "./logging/logger"; export { LogLevelDesc } from "loglevel"; export { Objects } from "./objects"; diff --git a/packages/cactus-common/src/test/typescript/unit/logging/log-helper.test.ts b/packages/cactus-common/src/test/typescript/unit/logging/log-helper.test.ts new file mode 100644 index 00000000000..23097d08df2 --- /dev/null +++ b/packages/cactus-common/src/test/typescript/unit/logging/log-helper.test.ts @@ -0,0 +1,301 @@ +import "jest-extended"; +import { RuntimeError } from "run-time-error"; +import { LogHelper } from "../../../../main/typescript/logging/log-helper"; + +describe("log-helper tests", () => { + const no_message_available = "NO_MESSAGE_INCLUDED_IN_EXCEPTION"; + const no_stack_available = "NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION"; + const errorMessage = "Oops"; + const errorNumber = 2468; + + describe("exception stack-tests", () => { + it("gets the stack information from a regular Error object", () => { + let expectedResult: string | undefined = ""; + let stack = no_stack_available; + + try { + const testError = new Error(errorMessage); + expectedResult = testError.stack; + throw testError; + } catch (error) { + stack = LogHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("gets stack information from a faked Error object which is containing stack information as string type", () => { + const expectedResult = "Faked stack"; + let stack = no_stack_available; + + const fakeErrorWithStack = { + message: + "This is a fake error object with string-type stack information", + stack: expectedResult, + }; + + try { + throw fakeErrorWithStack; + } catch (error) { + stack = LogHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("gets stack information from a faked Error object which is containing stack information as number type and therefore need to be stringified", () => { + const expectedResult = "123456"; + let stack = no_stack_available; + + const fakeErrorWithStack = { + message: + "This is a fake error object with number-type stack information", + stack: 123456, + }; + + try { + throw fakeErrorWithStack; + } catch (error) { + stack = LogHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("gets no stack information as the faked Error object is not containing any stack information", () => { + const expectedResult = no_stack_available; + let stack = no_stack_available; + + const fakeErrorWithoutStack = { + message: "This is a fake error object without stack information", + }; + + try { + throw fakeErrorWithoutStack; + } catch (error) { + stack = LogHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("handles throwing null successfully and returns NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_stack_available; + let stack = no_stack_available; + + const fakeError = null; + + try { + throw fakeError; + } catch (error) { + stack = LogHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + + it("handles throwing undefined successfully and returns NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_stack_available; + let stack = no_stack_available; + + const fakeError = undefined; + + try { + throw fakeError; + } catch (error) { + stack = LogHelper.getExceptionStack(error); + } + + // check stack + expect(stack).toBe(expectedResult); + }); + }); + + describe("exception message-tests", () => { + it("gets the exception message from a regular Error object", () => { + const expectedResult = errorMessage; + let message = no_message_available; + + try { + const testError = new Error(errorMessage); + throw testError; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("gets the exception message from a faked Error object which is containing message as string type", () => { + const expectedResult = errorMessage; + let message = no_message_available; + + const fakeErrorWithMessage = { + message: errorMessage, + stack: expectedResult, + }; + + try { + throw fakeErrorWithMessage; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("gets exception message from a faked Error object which is containing message information as number type and therefore need to be stringified", () => { + const expectedResult = "123456"; + let message = no_message_available; + + const fakeErrorWithNumberMessage = { + message: 123456, + }; + + try { + throw fakeErrorWithNumberMessage; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("gets no exception message information as the faked Error object is not containing any message information and therefore tries to stringify the whole exception", () => { + const expectedResult = + '{"stack":"This is a fake error object without message information"}'; + let message = no_message_available; + + const fakeErrorWithoutMessage = { + stack: "This is a fake error object without message information", + }; + + try { + throw fakeErrorWithoutMessage; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("handles throwing null successfully and returning NO_MESSAGE_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_message_available; + let message = no_message_available; + + const fakeError = null; + + try { + throw fakeError; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + + it("handles throwing undefined successfully and returning NO_MESSAGE_INCLUDED_IN_EXCEPTION string", () => { + const expectedResult = no_message_available; + let message = no_message_available; + + const fakeError = undefined; + + try { + throw fakeError; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + } + + // check message + expect(message).toBe(expectedResult); + }); + }); + + describe("handling of custom exceptions", () => { + it("handles a thrown string", () => { + const expectedErrorMessage = errorMessage; + const expectedStack = no_stack_available; + let message = no_message_available; + let stack = no_stack_available; + + try { + throw errorMessage; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + stack = LogHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toBe(expectedErrorMessage); + expect(stack).toBe(expectedStack); + }); + + it("handles a thrown number", () => { + const expectedErrorMessage = `${errorNumber}`; + const expectedStack = no_stack_available; + let message = no_message_available; + let stack = no_stack_available; + + try { + throw errorNumber; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + stack = LogHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toBe(expectedErrorMessage); + expect(stack).toBe(expectedStack); + }); + + it("handles an arbitrary exception", () => { + const expectedErrorMessage = '{"error":"Oops"}'; + const expectedStack = no_stack_available; + let message = no_message_available; + let stack = no_stack_available; + const arbitraryException = { error: errorMessage }; + + try { + throw arbitraryException; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + stack = LogHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toBe(expectedErrorMessage); + expect(stack).toBe(expectedStack); + }); + + it("handles nested exceptions", () => { + const expectedErrorMessage = "RTE3"; + const expectedStackPart = "RTE1"; + let message = no_message_available; + let stack = no_stack_available; + const rtE1 = new RuntimeError("RTE1"); + const rtE2 = new RuntimeError("RTE2", rtE1); + const rtE3 = new RuntimeError("RTE3", rtE2); + + try { + throw rtE3; + } catch (error) { + message = LogHelper.getExceptionMessage(error); + stack = LogHelper.getExceptionStack(error); + } + + // check message + stack + expect(message).toBe(expectedErrorMessage); + expect(stack).toContain(expectedStackPart); + }); + }); +});