diff --git a/packages/marshal/package.json b/packages/marshal/package.json index 21986b2982..e0eff7ed4b 100644 --- a/packages/marshal/package.json +++ b/packages/marshal/package.json @@ -41,6 +41,7 @@ }, "homepage": "https://github.com/endojs/endo#readme", "dependencies": { + "@endo/common": "^1.0.2", "@endo/errors": "^1.0.2", "@endo/eventual-send": "^1.1.0", "@endo/nat": "^5.0.2", diff --git a/packages/marshal/src/marshal-justin.js b/packages/marshal/src/marshal-justin.js index 240328774f..69bd2d7ed6 100644 --- a/packages/marshal/src/marshal-justin.js +++ b/packages/marshal/src/marshal-justin.js @@ -390,6 +390,10 @@ const decodeToJustin = (encoding, shouldIndent = false, slots = []) => { case 'error': { const { name, message } = rawTree; + // TODO cause, errors, AggregateError + // See https://github.com/endojs/endo/pull/2052 + name !== `AggregateError` || + Fail`AggregateError not yet implemented in marshal-justin`; return out.next(`${name}(${quote(message)})`); } diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index fe2b419e3b..edc8bb1d2c 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -9,6 +9,7 @@ import { } from '@endo/pass-style'; import { X, Fail, q, makeError, annotateError } from '@endo/errors'; +import { objectMap } from '@endo/common/object-map.js'; import { QCLASS, makeEncodeToCapData, @@ -260,11 +261,11 @@ export const makeMarshal = ( */ const decodeErrorCommon = (errData, decodeRecur) => { const { errorId = undefined, message, name, ...rest } = errData; - ownKeys(rest).length === 0 || - Fail`unexpected encoded error properties ${q(ownKeys(rest))}`; - // TODO Must decode `cause` and `errors` properties - // capData does not transform strings. The calls to `decodeRecur` - // are for reuse by other encodings that do, such as smallcaps. + // TODO Must decode `cause` and `errors` properties. + // See https://github.com/endojs/endo/pull/2052 + // capData does not transform strings. The immediately following calls + // to `decodeRecur` are for reuse by other encodings that do, + // such as smallcaps. const dName = decodeRecur(name); const dMessage = decodeRecur(message); const dErrorId = errorId && decodeRecur(errorId); @@ -279,6 +280,14 @@ export const makeMarshal = ( ? `Remote${EC.name}` : `Remote${EC.name}(${dErrorId})`; const error = makeError(dMessage, EC, { errorName }); + if (ownKeys(rest).length >= 1) { + // Note that this does not decodeRecur rest's property names. + // This would be inconsistent with smallcaps' expected handling, + // but is fine here since it is only used for `annotateError`, + // which is for diagnostic info that is otherwise unobservable. + const extras = objectMap(rest, decodeRecur); + annotateError(error, X`extra marshalled properties ${extras}`); + } return harden(error); }; diff --git a/packages/marshal/test/test-marshal-capdata.js b/packages/marshal/test/test-marshal-capdata.js index a828ac7e2f..38ba096da1 100644 --- a/packages/marshal/test/test-marshal-capdata.js +++ b/packages/marshal/test/test-marshal-capdata.js @@ -4,7 +4,13 @@ import { test } from './prepare-test-env-ava.js'; import { passStyleOf, makeTagged, Far } from '@endo/pass-style'; import { makeMarshal } from '../src/marshal.js'; -const { freeze, isFrozen, create, prototype: objectPrototype } = Object; +const { + freeze, + isFrozen, + create, + prototype: objectPrototype, + getPrototypeOf, +} = Object; const harden = /** @type {import('ses').Harden & { isFake?: boolean }} */ ( // eslint-disable-next-line no-undef @@ -295,6 +301,41 @@ test('unserialize errors', t => { t.is(em3.message, 'msg3'); }); +test('unserialize extended errors', t => { + const { unserialize } = makeTestMarshal(); + const uns = body => unserialize({ body, slots: [] }); + + // TODO cause, errors, and AggregateError will eventually be recognized. + // See https://github.com/endojs/endo/pull/2042 + + const refErr = uns( + '{"@qclass":"error","message":"msg","name":"ReferenceError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}', + ); + t.is(getPrototypeOf(refErr), ReferenceError.prototype); // direct instance of + t.false('extraProp' in refErr); + t.false('cause' in refErr); + t.false('errors' in refErr); + console.log('error with extra prop', refErr); + + const aggErr = uns( + '{"@qclass":"error","message":"msg","name":"AggregateError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}', + ); + t.is(getPrototypeOf(aggErr), Error.prototype); // direct instance of + t.false('extraProp' in aggErr); + t.false('cause' in aggErr); + t.false('errors' in aggErr); + console.log('error with extra prop', aggErr); + + const unkErr = uns( + '{"@qclass":"error","message":"msg","name":"UnknownError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}', + ); + t.is(getPrototypeOf(unkErr), Error.prototype); // direct instance of + t.false('extraProp' in unkErr); + t.false('cause' in unkErr); + t.false('errors' in unkErr); + console.log('error with extra prop', unkErr); +}); + test('passStyleOf null is "null"', t => { t.assert(passStyleOf(null), 'null'); }); diff --git a/packages/marshal/test/test-marshal-smallcaps.js b/packages/marshal/test/test-marshal-smallcaps.js index d413e87b68..714e155596 100644 --- a/packages/marshal/test/test-marshal-smallcaps.js +++ b/packages/marshal/test/test-marshal-smallcaps.js @@ -6,7 +6,13 @@ import { makeMarshal } from '../src/marshal.js'; import { roundTripPairs } from './test-marshal-capdata.js'; -const { freeze, isFrozen, create, prototype: objectPrototype } = Object; +const { + freeze, + isFrozen, + create, + prototype: objectPrototype, + getPrototypeOf, +} = Object; const harden = /** @type {import('ses').Harden & { isFake?: boolean }} */ ( // eslint-disable-next-line no-undef @@ -153,6 +159,41 @@ test('smallcaps unserialize errors', t => { t.is(em3.message, 'msg3'); }); +test('smallcaps unserialize extended errors', t => { + const { unserialize } = makeTestMarshal(); + const uns = body => unserialize({ body, slots: [] }); + + // TODO cause, errors, and AggregateError will eventually be recognized. + // See https://github.com/endojs/endo/pull/2042 + + const refErr = uns( + '#{"#error":"msg","name":"ReferenceError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}', + ); + t.is(getPrototypeOf(refErr), ReferenceError.prototype); // direct instance of + t.false('extraProp' in refErr); + t.false('cause' in refErr); + t.false('errors' in refErr); + console.log('error with extra prop', refErr); + + const aggErr = uns( + '#{"#error":"msg","name":"AggregateError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}', + ); + t.is(getPrototypeOf(aggErr), Error.prototype); // direct instance of + t.false('extraProp' in aggErr); + t.false('cause' in aggErr); + t.false('errors' in aggErr); + console.log('error with extra prop', aggErr); + + const unkErr = uns( + '#{"#error":"msg","name":"UnknownError","extraProp":"foo","cause":"bar","errors":["zip","zap"]}', + ); + t.is(getPrototypeOf(unkErr), Error.prototype); // direct instance of + t.false('extraProp' in unkErr); + t.false('cause' in unkErr); + t.false('errors' in unkErr); + console.log('error with extra prop', unkErr); +}); + test('smallcaps mal-formed @qclass', t => { const { unserialize } = makeTestMarshal(); const uns = body => unserialize({ body, slots: [] });