forked from hyperledger-cacti/cacti
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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: hyperledger-cacti#1702 Signed-off-by: Michael Courtin <[email protected]>
- Loading branch information
Showing
7 changed files
with
785 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,6 +52,11 @@ | |
"name": "Peter Somogyvari", | ||
"email": "[email protected]", | ||
"url": "https://accenture.com" | ||
}, | ||
{ | ||
"name": "Michael Courtin", | ||
"email": "[email protected]", | ||
"url": "https://accenture.com" | ||
} | ||
], | ||
"license": "Apache-2.0", | ||
|
@@ -60,6 +65,7 @@ | |
}, | ||
"homepage": "https://github.com/hyperledger/cactus#readme", | ||
"dependencies": { | ||
"fast-safe-stringify": "2.1.1", | ||
"json-stable-stringify": "1.0.1", | ||
"key-encoder": "2.0.3", | ||
"loglevel": "1.7.1", | ||
|
327 changes: 327 additions & 0 deletions
327
packages/cactus-common/src/main/typescript/exception/exception-helper.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,327 @@ | ||
import { RuntimeError } from "run-time-error"; | ||
import stringify from "fast-safe-stringify"; | ||
import { Logger } from "../logging/logger"; | ||
|
||
export class ExceptionHelper { | ||
/** | ||
* Fetches the exception-content and submits it to the logger | ||
* | ||
* @param logger logger created thru the LoggerProvider.getOrCreate() | ||
* @param exception as an arbitrary exception of whatever type and format it might be | ||
* @param optionalAdditionalContext as optional on top info / context on the exception scenario | ||
*/ | ||
public static logException( | ||
logger: Logger, | ||
exception: unknown, | ||
optionalAdditionalContext = "", | ||
): void { | ||
const exceptionContent = this.getExceptionContent( | ||
exception, | ||
optionalAdditionalContext, | ||
); | ||
|
||
// log to destination | ||
logger.error(...exceptionContent); | ||
} | ||
|
||
/** | ||
* Creates a new RuntimeError based on the incoming exception and context to preserve the complete hierarchy of exception information | ||
* | ||
* @param exception as an arbitrary exception of whatever type and format it might be | ||
* @param context providing context about the exception scenario | ||
* @returns a new RuntimeError created from exception to preserve the complete hierarchy of exception information | ||
*/ | ||
public static getNewRuntimeErrorFromException( | ||
exception: unknown, | ||
context: string, | ||
): RuntimeError { | ||
const exceptionMessageInfo = this.getExceptionMessageInfo(exception); | ||
const exceptionStackInfo = this.getExceptionStackInfo(exception); | ||
let content = ""; | ||
let runtimeError = new RuntimeError(context, exceptionMessageInfo.message); | ||
|
||
if (exception instanceof RuntimeError || exception instanceof Error) { | ||
// scenario 1: exception is already an instance of Error / RuntimeError -> can be used directly | ||
runtimeError = new RuntimeError(context, exception); | ||
} else { | ||
// scenario 2: exception is of custom type | ||
// -> need to fetch content | ||
if (exceptionMessageInfo.containsCompleteStringifiedException) { | ||
content = exceptionMessageInfo.message; | ||
} else { | ||
content = | ||
exceptionMessageInfo.message + " - " + exceptionStackInfo.stack; | ||
} | ||
|
||
runtimeError = new RuntimeError(context, content); | ||
} | ||
|
||
return runtimeError; | ||
} | ||
|
||
/** | ||
* | ||
* @param exception as an arbitrary exception of whatever type and format it might be | ||
* @param optionalAdditionalContext as optional on top info / context on the exception scenario | ||
* @returns an array of string information about the exception like: | ||
* - message, | ||
* - stack | ||
* - other properties (depending on the type of exception) | ||
*/ | ||
public static getExceptionContent( | ||
exception: unknown, | ||
optionalAdditionalContext = "", | ||
): string[] { | ||
const exceptionStackInfo = this.getExceptionStackInfo(exception); | ||
const exceptionMessageInfo = this.getExceptionMessageInfo(exception); | ||
const validOptionalAdditionalContext = | ||
optionalAdditionalContext && | ||
optionalAdditionalContext !== "" && | ||
optionalAdditionalContext !== " "; | ||
let content: string[] = []; | ||
let done = false; | ||
|
||
// scenario 1: messageInfo.message is containing a fully stringified exception -> no need to additionally fetch for the exception stack | ||
// and some valid additional context is provided | ||
if ( | ||
exceptionMessageInfo.containsCompleteStringifiedException && | ||
validOptionalAdditionalContext | ||
) { | ||
content = [optionalAdditionalContext, exceptionMessageInfo.message]; | ||
done = true; | ||
} | ||
|
||
// scenario 2: messageInfo.message is containing a fully stringified exception -> no need to additionally fetch for the exception stack | ||
// and no valid additional context is provided | ||
if ( | ||
!done && | ||
exceptionMessageInfo.containsCompleteStringifiedException && | ||
!validOptionalAdditionalContext | ||
) { | ||
content = [exceptionMessageInfo.message]; | ||
} | ||
|
||
// scenario 3: messageInfo.message is not containing a fully stringified exception -> need to fetch also exception stack | ||
// and some valid additional context is provided | ||
if ( | ||
!done && | ||
!exceptionMessageInfo.containsCompleteStringifiedException && | ||
validOptionalAdditionalContext | ||
) { | ||
content = [ | ||
optionalAdditionalContext, | ||
exceptionMessageInfo.message, | ||
exceptionStackInfo.stack, | ||
]; | ||
} | ||
|
||
// scenario 4: messageInfo.message is not containing a fully stringified exception -> need to fetch also exception stack | ||
// and no valid additional context is provided | ||
if ( | ||
!done && | ||
!exceptionMessageInfo.containsCompleteStringifiedException && | ||
!validOptionalAdditionalContext | ||
) { | ||
content = [exceptionMessageInfo.message, exceptionStackInfo.stack]; | ||
} | ||
|
||
return content; | ||
} | ||
|
||
/** | ||
* USE THIS FUNCTION ONLY IN CASE OF SPECIAL EXCEPTION HANDLING | ||
* For a general exception handling / logging use the function logException() or consoleException() above | ||
* | ||
* @param exception as an arbitrary exception of whatever type and format it might be | ||
* @returns the message information of the exception as a string | ||
*/ | ||
public static getExceptionMessage(exception: unknown): string { | ||
const exceptionMessageInfo = this.getExceptionMessageInfo(exception); | ||
|
||
return exceptionMessageInfo.message; | ||
} | ||
|
||
/** | ||
* USE THIS FUNCTION ONLY IN CASE OF SPECIAL EXCEPTION HANDLING | ||
* For a general exception handling / logging use the function logException() or consoleException() above | ||
* | ||
* @param exception as an arbitrary exception of whatever type and format it might be | ||
* @returns the stack information of the exception as a string | ||
*/ | ||
public static getExceptionStack(exception: unknown): string { | ||
const exceptionStackInfo = this.getExceptionStackInfo(exception); | ||
|
||
return exceptionStackInfo.stack; | ||
} | ||
|
||
/** | ||
* private helper method to obtain the message information of the exception as a message-string and | ||
* an additional indicator if the message contains a fully stringified exception | ||
* | ||
* @param exception as an arbitrary exception of whatever type it might be | ||
* @returns the message information of the exception as a message-string and an indicator if the message contains fully stringified exception | ||
*/ | ||
private static getExceptionMessageInfo( | ||
exception: unknown, | ||
): { message: string; containsCompleteStringifiedException: boolean } { | ||
// 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; | ||
let containsCompleteStringifiedException = false; | ||
|
||
// 1st need to check that exception is not null or undefined before trying to access the wanted message information | ||
if (exception) { | ||
const isOfTypeString = typeof exception === "string"; | ||
const isOfTypeObject = typeof exception === "object"; | ||
const hasOwnPropertyMessage = Object.hasOwnProperty.call( | ||
exception, | ||
"message", | ||
); | ||
const messageIsOfTypeString = | ||
typeof (exception as Record<string, unknown>).message === "string"; | ||
|
||
// scenario 1: exception is of type object and providing a string message property | ||
if (isOfTypeObject && hasOwnPropertyMessage && messageIsOfTypeString) { | ||
message = (exception as { message: string }).message; | ||
exceptionHandled = true; | ||
} | ||
|
||
// scenario 2: exception is of type object and providing a non-string message property | ||
if ( | ||
!exceptionHandled && | ||
isOfTypeObject && | ||
hasOwnPropertyMessage && | ||
!messageIsOfTypeString | ||
) { | ||
// need to stringify message information first | ||
message = this.safeJsonStringify( | ||
(exception as { message: unknown }).message, | ||
invalidMessage, | ||
); | ||
exceptionHandled = true; | ||
} | ||
|
||
// scenario 3: handling of string type exceptions | ||
if (!exceptionHandled && isOfTypeString) { | ||
message = customExceptionPrefix + exception; | ||
exceptionHandled = true; | ||
} | ||
|
||
// scenario 4: handling of custom exceptions | ||
if (!exceptionHandled && !isOfTypeString) { | ||
// custom exception is of a different type -> need to stringify it | ||
message = | ||
customExceptionPrefix + | ||
this.safeJsonStringify(exception, invalidException); | ||
exceptionHandled = true; | ||
containsCompleteStringifiedException = true; | ||
} | ||
} | ||
return { | ||
message, | ||
containsCompleteStringifiedException, | ||
}; | ||
} | ||
|
||
/** | ||
* private helper method to obtain the stack information of the exception as a stack-string and | ||
* an additional indicator if the stack contains a fully stringified exception | ||
* | ||
* @param exception as an arbitrary exception of whatever type it might be | ||
* @returns the stack information of the exception as a stack-string and an indicator if the stack contains fully stringified exception | ||
*/ | ||
private static getExceptionStackInfo( | ||
exception: unknown, | ||
): { stack: string; containsCompleteStringifiedException: boolean } { | ||
// handle unknown exception input | ||
const fallbackStack = "NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION"; | ||
const invalidStack = "INVALID_STACK_INFORMATION"; | ||
const invalidException = "INVALID_EXCEPTION"; | ||
const customExceptionPrefix = "A CUSTOM EXCEPTION WAS THROWN: "; | ||
let stack = fallbackStack; | ||
let exceptionHandled = false; | ||
let containsCompleteStringifiedException = false; | ||
|
||
// 1st need to check that exception is not null or undefined before trying to access the wanted stack information | ||
// otherwise the default fallback stack info will be returned | ||
if (exception) { | ||
const isOfTypeObject = typeof exception === "object"; | ||
const isInstanceOfRuntimeError = exception instanceof RuntimeError; | ||
const hasOwnPropertyStack = Object.hasOwnProperty.call( | ||
exception, | ||
"stack", | ||
); | ||
const stackIsOfTypeString = | ||
typeof (exception as Record<string, unknown>).stack === "string"; | ||
|
||
// scenario 1: exception is an instance of RuntimeError | ||
if (isInstanceOfRuntimeError) { | ||
// handling RuntimeError stack inclusive nested / cascaded stacks | ||
stack = this.safeJsonStringify(exception); | ||
containsCompleteStringifiedException = true; | ||
exceptionHandled = true; | ||
} | ||
|
||
// scenario 2: exception is of type object and providing a string stack property | ||
if ( | ||
!exceptionHandled && | ||
isOfTypeObject && | ||
hasOwnPropertyStack && | ||
stackIsOfTypeString | ||
) { | ||
stack = (exception as { stack: string }).stack; | ||
exceptionHandled = true; | ||
} | ||
|
||
// scenario 3: exception is of type object and providing a non-string stack property | ||
if ( | ||
!exceptionHandled && | ||
isOfTypeObject && | ||
hasOwnPropertyStack && | ||
!stackIsOfTypeString | ||
) { | ||
// need to stringify stack information first | ||
stack = this.safeJsonStringify( | ||
(exception as { stack: unknown }).stack, | ||
invalidStack, | ||
); | ||
containsCompleteStringifiedException = true; | ||
exceptionHandled = true; | ||
} | ||
|
||
// scenario 4: handling of custom exceptions | ||
if (!exceptionHandled) { | ||
// custom exception is of a different type -> need to stringify it | ||
stack = | ||
customExceptionPrefix + | ||
this.safeJsonStringify(exception, invalidException); | ||
containsCompleteStringifiedException = true; | ||
exceptionHandled = true; | ||
} | ||
} | ||
return { | ||
stack, | ||
containsCompleteStringifiedException, | ||
}; | ||
} | ||
|
||
private static safeJsonStringify( | ||
input: unknown, | ||
catchMessage = "INVALID_INPUT", | ||
): string { | ||
let message = ""; | ||
|
||
try { | ||
// use fast-safe-stringify to also handle gracefully circular structures | ||
message = stringify(input); | ||
} catch (error) { | ||
// fast and safe stringify failed | ||
message = catchMessage; | ||
} | ||
return message; | ||
} | ||
} |
Oops, something went wrong.