Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Preserve original messages during error serialization by default #158

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = {
// An object that configures minimum threshold enforcement for coverage results
coverageThreshold: {
global: {
branches: 92.59,
branches: 92.77,
functions: 94.73,
lines: 92.64,
statements: 92.64,
Expand Down
100 changes: 59 additions & 41 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { dataHasCause, getMessageFromCode, serializeError } from './utils';
const rpcCodes = errorCodes.rpc;

describe('serializeError', () => {
it('handles invalid error: non-object', () => {
it('serializes invalid error: non-object', () => {
const result = serializeError(invalidError0);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -30,7 +30,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: null', () => {
it('serializes invalid error: null', () => {
const result = serializeError(invalidError5);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -39,7 +39,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: undefined', () => {
it('serializes invalid error: undefined', () => {
const result = serializeError(invalidError6);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -48,7 +48,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: array', () => {
it('serializes invalid error: array', () => {
const result = serializeError(invalidError1);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -57,7 +57,27 @@ describe('serializeError', () => {
});
});

it('handles invalid error: invalid code', () => {
it('serializes invalid error: array with non-JSON values', () => {
const error = ['foo', Symbol('bar'), { baz: 'qux', symbol: Symbol('') }];
const result = serializeError(error);
expect(result).toStrictEqual({
code: rpcCodes.internal,
message: getMessageFromCode(rpcCodes.internal),
data: {
cause: ['foo', null, { baz: 'qux' }],
},
});

expect(JSON.parse(JSON.stringify(result))).toStrictEqual({
code: rpcCodes.internal,
message: getMessageFromCode(rpcCodes.internal),
data: {
cause: ['foo', null, { baz: 'qux' }],
},
});
});

it('serializes invalid error: invalid code', () => {
const result = serializeError(invalidError2);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -66,7 +86,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: valid code, undefined message', () => {
it('serializes invalid error: valid code, undefined message', () => {
const result = serializeError(invalidError3);
expect(result).toStrictEqual({
code: errorCodes.rpc.internal,
Expand All @@ -79,7 +99,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: non-string message with data', () => {
it('serializes invalid error: non-string message with data', () => {
const result = serializeError(invalidError4);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -94,7 +114,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: invalid code with string message', () => {
it('serializes invalid error: invalid code with string message', () => {
const result = serializeError(invalidError7);
expect(result).toStrictEqual({
code: rpcCodes.internal,
Expand All @@ -109,7 +129,7 @@ describe('serializeError', () => {
});
});

it('handles invalid error: invalid code, no message, custom fallback', () => {
it('serializes invalid error: invalid code, no message, custom fallback', () => {
const result = serializeError(invalidError2, {
fallbackError: { code: rpcCodes.methodNotFound, message: 'foo' },
});
Expand All @@ -120,15 +140,15 @@ describe('serializeError', () => {
});
});

it('handles valid error: code and message only', () => {
it('serializes valid error: code and message only', () => {
const result = serializeError(validError0);
expect(result).toStrictEqual({
code: 4001,
message: validError0.message,
});
});

it('handles valid error: code, message, and data', () => {
it('serializes valid error: code, message, and data', () => {
const result = serializeError(validError1);
expect(result).toStrictEqual({
code: 4001,
Expand All @@ -137,23 +157,23 @@ describe('serializeError', () => {
});
});

it('handles valid error: instantiated error', () => {
it('serializes valid error: instantiated error', () => {
const result = serializeError(validError2);
expect(result).toStrictEqual({
code: rpcCodes.parse,
message: getMessageFromCode(rpcCodes.parse),
});
});

it('handles valid error: other instantiated error', () => {
it('serializes valid error: other instantiated error', () => {
const result = serializeError(validError3);
expect(result).toStrictEqual({
code: rpcCodes.parse,
message: dummyMessage,
});
});

it('handles valid error: instantiated error with custom message and data', () => {
it('serializes valid error: instantiated error with custom message and data', () => {
const result = serializeError(validError4);
expect(result).toStrictEqual({
code: rpcCodes.parse,
Expand All @@ -162,7 +182,7 @@ describe('serializeError', () => {
});
});

it('handles valid error: message and data', () => {
it('serializes valid error: message and data', () => {
const result = serializeError(Object.assign({}, validError1));
expect(result).toStrictEqual({
code: 4001,
Expand All @@ -171,7 +191,7 @@ describe('serializeError', () => {
});
});

it('handles including stack: no stack present', () => {
it('serializes valid error: no stack present', () => {
const result = serializeError(validError1);
expect(result).toStrictEqual({
code: 4001,
Expand All @@ -180,7 +200,7 @@ describe('serializeError', () => {
});
});

it('handles including stack: string stack present', () => {
it('serializes valid error: string stack present', () => {
const result = serializeError(
Object.assign({}, validError1, { stack: 'foo' }),
);
Expand All @@ -192,7 +212,7 @@ describe('serializeError', () => {
});
});

it('handles removing stack', () => {
it('removes the stack with `shouldIncludeStack: false`', () => {
const result = serializeError(
Object.assign({}, validError1, { stack: 'foo' }),
{ shouldIncludeStack: false },
Expand All @@ -204,7 +224,25 @@ describe('serializeError', () => {
});
});

it('handles regular Error()', () => {
it('overwrites the original message with `shouldPreserveMessage: false`', () => {
const error = new Error('foo');
const result = serializeError(error, {
shouldPreserveMessage: false,
fallbackError: validError0,
});
expect(result).toStrictEqual({
code: validError0.code,
message: validError0.message,
data: {
cause: {
message: error.message,
stack: error.stack,
},
},
});
});
Comment on lines +227 to +243
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only addition to this file.


it('serializes invalid error: Error', () => {
const error = new Error('foo');
const result = serializeError(error);
expect(result).toStrictEqual({
Expand All @@ -230,7 +268,7 @@ describe('serializeError', () => {
});
});

it('handles JsonRpcError', () => {
it('serializes valid error: JsonRpcError', () => {
const error = rpcErrors.invalidParams();
const result = serializeError(error);
expect(result).toStrictEqual({
Expand All @@ -246,7 +284,7 @@ describe('serializeError', () => {
});
});

it('handles class that has serialize function', () => {
it('serializes error with serialize() method', () => {
class MockClass {
serialize() {
return { code: 1, message: 'foo' };
Expand Down Expand Up @@ -289,26 +327,6 @@ describe('serializeError', () => {
'Must provide fallback error with integer number code and string message.',
);
});

it('handles arrays passed as error', () => {
const error = ['foo', Symbol('bar'), { baz: 'qux', symbol: Symbol('') }];
const result = serializeError(error);
expect(result).toStrictEqual({
code: rpcCodes.internal,
message: getMessageFromCode(rpcCodes.internal),
data: {
cause: ['foo', null, { baz: 'qux' }],
},
});

expect(JSON.parse(JSON.stringify(result))).toStrictEqual({
code: rpcCodes.internal,
message: getMessageFromCode(rpcCodes.internal),
data: {
cause: ['foo', null, { baz: 'qux' }],
},
});
});
});

describe('dataHasCause', () => {
Expand Down
24 changes: 17 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,22 +96,28 @@ export function isValidCode(code: unknown): code is number {
* @param error - The error to serialize.
* @param options - Options bag.
* @param options.fallbackError - The error to return if the given error is
* not compatible. Should be a JSON serializable value.
* not compatible. Should be a JSON-serializable value.
* @param options.shouldIncludeStack - Whether to include the error's stack
* on the returned object.
* @param options.shouldPreserveMessage - Whether to preserve the error's
* message if the fallback error is used.
* @returns The serialized error.
*/
export function serializeError(
error: unknown,
{ fallbackError = FALLBACK_ERROR, shouldIncludeStack = true } = {},
{
fallbackError = FALLBACK_ERROR,
shouldIncludeStack = true,
shouldPreserveMessage = true,
} = {},
): SerializedJsonRpcError {
if (!isJsonRpcError(fallbackError)) {
throw new Error(
'Must provide fallback error with integer number code and string message.',
);
}

const serialized = buildError(error, fallbackError);
const serialized = buildError(error, fallbackError, shouldPreserveMessage);

if (!shouldIncludeStack) {
delete serialized.stack;
Expand All @@ -121,15 +127,18 @@ export function serializeError(
}

/**
* Construct a JSON-serializable object given an error and a JSON serializable `fallbackError`
* Construct a JSON-serializable object given an error and a JSON-serializable `fallbackError`
*
* @param error - The error in question.
* @param fallbackError - A JSON serializable fallback error.
* @returns A JSON serializable error object.
* @param fallbackError - A JSON-serializable fallback error.
* @param shouldPreserveMessage - Whether to preserve the error's message if the fallback
* error is used.
* @returns A JSON-serializable error object.
*/
function buildError(
error: unknown,
fallbackError: SerializedJsonRpcError,
shouldPreserveMessage: boolean,
): SerializedJsonRpcError {
// If an error specifies a `serialize` function, we call it and return the result.
if (
Expand All @@ -151,7 +160,8 @@ function buildError(
const cause = serializeCause(error);
const fallbackWithCause = {
...fallbackError,
...(originalMessage && { message: originalMessage }),
...(shouldPreserveMessage &&
originalMessage && { message: originalMessage }),
data: { cause },
};

Expand Down