Skip to content

Commit b1b3646

Browse files
committed
fix: Handle errors properly in asV2Middleware
1 parent ef766a8 commit b1b3646

File tree

6 files changed

+40
-21
lines changed

6 files changed

+40
-21
lines changed

packages/eth-json-rpc-provider/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- **BREAKING:** Use `JsonRpcServer` instead of `JsonRpcEngine` ([#7001](https://github.com/MetaMask/core/pull/7001))
1616
- Adds a new `server` constructor option to the `InternalProvider` class, mutually exclusive with the now deprecated `engine` option.
1717
- Legacy `JsonRpcEngine` instances are wrapped in a `JsonRpcServer` internally
18-
wherever they appear. Due to differences in error serialization, this may be
19-
breaking for consumers.
18+
wherever they appear. Due to differences in error serialization, this may be
19+
breaking for consumers.
2020

2121
## [5.0.1]
2222

packages/eth-json-rpc-provider/src/internal-provider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,8 @@ export function convertEip1193RequestToJsonRpcRequest<
197197
}
198198

199199
/**
200-
* Deserialize a JSON-RPC error.
200+
* Deserialize a JSON-RPC error. Ignores the possibility of `stack` property, since this is
201+
* stripped by `JsonRpcServer`.
201202
*
202203
* @param error - The JSON-RPC error to deserialize.
203204
* @returns The deserialized error.

packages/json-rpc-engine/src/asV2Middleware.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,21 @@ describe('asV2Middleware', () => {
171171
);
172172
});
173173

174+
it('does not forward undefined errors from legacy middleware', async () => {
175+
const legacyMiddleware = jest.fn((_req, res, _next, end) => {
176+
res.error = undefined;
177+
res.result = 42;
178+
end();
179+
});
180+
181+
const v2Engine = JsonRpcEngineV2.create({
182+
middleware: [asV2Middleware(legacyMiddleware)],
183+
});
184+
185+
const result = await v2Engine.handle(makeRequest());
186+
expect(result).toBe(42);
187+
});
188+
174189
it('allows v2 engine to continue when legacy middleware does not end', async () => {
175190
const legacyMiddleware = jest.fn((_req, _res, next) => {
176191
next();

packages/json-rpc-engine/src/asV2Middleware.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
fromLegacyRequest,
2020
propagateToContext,
2121
propagateToRequest,
22-
unserializeError,
22+
deserializeError,
2323
} from './v2/compatibility-utils';
2424
import type {
2525
// Used in docs.
@@ -104,8 +104,11 @@ export function asV2Middleware<
104104
});
105105
propagateToContext(req, context);
106106

107-
if (hasProperty(response, 'error')) {
108-
throw unserializeError(response.error);
107+
// Mimic the behavior of JsonRpcEngine.#handle(), which only treats truthy errors as errors.
108+
// Legacy middleware may violate the invariant that response objects have either a result or an
109+
// error property. In practice, we may see response objects with results and `{ error: undefined }`.
110+
if (hasProperty(response, 'error') && response.error) {
111+
throw deserializeError(response.error);
109112
} else if (hasProperty(response, 'result')) {
110113
return response.result as ResultConstraint<Request>;
111114
}

packages/json-rpc-engine/src/v2/compatibility-utils.test.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
makeContext,
88
propagateToContext,
99
propagateToRequest,
10-
unserializeError,
10+
deserializeError,
1111
} from './compatibility-utils';
1212
import { MiddlewareContext } from './MiddlewareContext';
1313
import { stringify } from './utils';
@@ -348,7 +348,7 @@ describe('compatibility-utils', () => {
348348
});
349349
});
350350

351-
describe('unserializeError', () => {
351+
describe('deserializeError', () => {
352352
// Requires some special handling due to the possible existence or
353353
// non-existence of Error.isError
354354
describe('Error.isError', () => {
@@ -382,22 +382,22 @@ describe('compatibility-utils', () => {
382382
isError.mockReturnValueOnce(true);
383383
const originalError = new Error('test error');
384384

385-
const result = unserializeError(originalError);
385+
const result = deserializeError(originalError);
386386
expect(result).toBe(originalError);
387387
});
388388

389389
it('returns the thrown value when it is instanceof Error', () => {
390390
isError.mockReturnValueOnce(false);
391391
const originalError = new Error('test error');
392392

393-
const result = unserializeError(originalError);
393+
const result = deserializeError(originalError);
394394
expect(result).toBe(originalError);
395395
});
396396
});
397397

398398
it('creates a new Error when thrown value is a string', () => {
399399
const errorMessage = 'test error message';
400-
const result = unserializeError(errorMessage);
400+
const result = deserializeError(errorMessage);
401401

402402
expect(result).toBeInstanceOf(Error);
403403
expect(result.message).toBe(errorMessage);
@@ -406,7 +406,7 @@ describe('compatibility-utils', () => {
406406
it.each([42, true, false, null, undefined, Symbol('test')])(
407407
'creates a new Error with stringified message for non-object values',
408408
(value) => {
409-
const result = unserializeError(value);
409+
const result = deserializeError(value);
410410

411411
expect(result).toBeInstanceOf(Error);
412412
expect(result.message).toBe(`Unknown error: ${stringify(value)}`);
@@ -421,7 +421,7 @@ describe('compatibility-utils', () => {
421421
data: { foo: 'bar' },
422422
};
423423

424-
const result = unserializeError(thrownValue);
424+
const result = deserializeError(thrownValue);
425425

426426
expect(result).toBeInstanceOf(JsonRpcError);
427427
expect(result).toMatchObject({
@@ -439,7 +439,7 @@ describe('compatibility-utils', () => {
439439
data: { foo: 'bar' },
440440
};
441441

442-
const result = unserializeError(thrownValue);
442+
const result = deserializeError(thrownValue);
443443

444444
expect(result).toBeInstanceOf(Error);
445445
expect(result).not.toBeInstanceOf(JsonRpcError);
@@ -455,7 +455,7 @@ describe('compatibility-utils', () => {
455455
message: 'test error message',
456456
};
457457

458-
const result = unserializeError(thrownValue);
458+
const result = deserializeError(thrownValue);
459459

460460
expect(result).toBeInstanceOf(Error);
461461
expect(result).not.toBeInstanceOf(JsonRpcError);
@@ -469,7 +469,7 @@ describe('compatibility-utils', () => {
469469
stack: stackTrace,
470470
};
471471

472-
const result = unserializeError(thrownValue);
472+
const result = deserializeError(thrownValue);
473473

474474
expect(result).toBeInstanceOf(Error);
475475
expect(result.stack).toBe(stackTrace);
@@ -485,7 +485,7 @@ describe('compatibility-utils', () => {
485485
data,
486486
};
487487

488-
const result = unserializeError(thrownValue) as JsonRpcError<Json>;
488+
const result = deserializeError(thrownValue) as JsonRpcError<Json>;
489489

490490
expect(result.cause).toBe(cause);
491491
expect(result.data).toStrictEqual({
@@ -499,7 +499,7 @@ describe('compatibility-utils', () => {
499499
code: 1234,
500500
};
501501

502-
const result = unserializeError(thrownValue);
502+
const result = deserializeError(thrownValue);
503503

504504
expect(result.message).toBe('Unknown error');
505505
});
@@ -510,7 +510,7 @@ describe('compatibility-utils', () => {
510510
message: 42,
511511
};
512512

513-
const result = unserializeError(thrownValue);
513+
const result = deserializeError(thrownValue);
514514

515515
expect(result.message).toBe('Unknown error');
516516
});
@@ -521,7 +521,7 @@ describe('compatibility-utils', () => {
521521
message: 42,
522522
};
523523

524-
const result = unserializeError(thrownValue);
524+
const result = deserializeError(thrownValue);
525525

526526
expect(result.message).toBe('Internal JSON-RPC error.');
527527
});

packages/json-rpc-engine/src/v2/compatibility-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function propagateToRequest(
135135
* @param thrown - The thrown value to unserialize.
136136
* @returns The unserialized error.
137137
*/
138-
export function unserializeError(thrown: unknown): Error | JsonRpcError<Json> {
138+
export function deserializeError(thrown: unknown): Error | JsonRpcError<Json> {
139139
// @ts-expect-error - New, but preferred if available.
140140
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError
141141
if (typeof Error.isError === 'function' && Error.isError(thrown)) {

0 commit comments

Comments
 (0)