Skip to content

Commit 9ad42b7

Browse files
addaleaxtargos
authored andcommitted
worker: improve error (de)serialization
Rather than passing errors using some sort of string representation, do a best effort for faithful serialization/deserialization of uncaught exception objects. PR-URL: #20876 Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Shingo Inoue <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]> Reviewed-By: John-David Dalton <[email protected]> Reviewed-By: Gus Caplan <[email protected]>
1 parent ecba1c5 commit 9ad42b7

File tree

6 files changed

+171
-16
lines changed

6 files changed

+171
-16
lines changed

‎lib/internal/error-serdes.js‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use strict';
2+
3+
constBuffer=require('buffer').Buffer;
4+
const{ serialize, deserialize }=require('v8');
5+
const{ SafeSet }=require('internal/safe_globals');
6+
7+
constkSerializedError=0;
8+
constkSerializedObject=1;
9+
constkInspectedError=2;
10+
11+
constGetPrototypeOf=Object.getPrototypeOf;
12+
constGetOwnPropertyDescriptor=Object.getOwnPropertyDescriptor;
13+
constGetOwnPropertyNames=Object.getOwnPropertyNames;
14+
constDefineProperty=Object.defineProperty;
15+
constAssign=Object.assign;
16+
constObjectPrototypeToString=
17+
Function.prototype.call.bind(Object.prototype.toString);
18+
constForEach=Function.prototype.call.bind(Array.prototype.forEach);
19+
constCall=Function.prototype.call.bind(Function.prototype.call);
20+
21+
consterrors={
22+
Error, TypeError, RangeError, URIError, SyntaxError, ReferenceError, EvalError
23+
};
24+
consterrorConstructorNames=newSafeSet(Object.keys(errors));
25+
26+
functionTryGetAllProperties(object,target=object){
27+
constall=Object.create(null);
28+
if(object===null)
29+
returnall;
30+
Assign(all,TryGetAllProperties(GetPrototypeOf(object),target));
31+
constkeys=GetOwnPropertyNames(object);
32+
ForEach(keys,(key)=>{
33+
constdescriptor=GetOwnPropertyDescriptor(object,key);
34+
constgetter=descriptor.get;
35+
if(getter&&key!=='__proto__'){
36+
try{
37+
descriptor.value=Call(getter,target);
38+
}catch{}
39+
}
40+
if('value'indescriptor&&typeofdescriptor.value!=='function'){
41+
deletedescriptor.get;
42+
deletedescriptor.set;
43+
all[key]=descriptor;
44+
}
45+
});
46+
returnall;
47+
}
48+
49+
functionGetConstructors(object){
50+
constconstructors=[];
51+
52+
for(varcurrent=object;
53+
current!==null;
54+
current=GetPrototypeOf(current)){
55+
constdesc=GetOwnPropertyDescriptor(current,'constructor');
56+
if(desc&&desc.value){
57+
DefineProperty(constructors,constructors.length,{
58+
value: desc.value,enumerable: true
59+
});
60+
}
61+
}
62+
63+
returnconstructors;
64+
}
65+
66+
functionGetName(object){
67+
constdesc=GetOwnPropertyDescriptor(object,'name');
68+
returndesc&&desc.value;
69+
}
70+
71+
letutil;
72+
functionlazyUtil(){
73+
if(!util)
74+
util=require('util');
75+
returnutil;
76+
}
77+
78+
functionserializeError(error){
79+
try{
80+
if(typeoferror==='object'&&
81+
ObjectPrototypeToString(error)==='[object Error]'){
82+
constconstructors=GetConstructors(error);
83+
for(vari=constructors.length-1;i>=0;i--){
84+
constname=GetName(constructors[i]);
85+
if(errorConstructorNames.has(name)){
86+
try{error.stack;}catch{}
87+
constserialized=serialize({
88+
constructor: name,
89+
properties: TryGetAllProperties(error)
90+
});
91+
returnBuffer.concat([Buffer.from([kSerializedError]),serialized]);
92+
}
93+
}
94+
}
95+
}catch{}
96+
try{
97+
constserialized=serialize(error);
98+
returnBuffer.concat([Buffer.from([kSerializedObject]),serialized]);
99+
}catch{}
100+
returnBuffer.concat([Buffer.from([kInspectedError]),
101+
Buffer.from(lazyUtil().inspect(error),'utf8')]);
102+
}
103+
104+
functiondeserializeError(error){
105+
switch(error[0]){
106+
casekSerializedError:
107+
const{ constructor, properties }=deserialize(error.subarray(1));
108+
constctor=errors[constructor];
109+
returnObject.create(ctor.prototype,properties);
110+
casekSerializedObject:
111+
returndeserialize(error.subarray(1));
112+
casekInspectedError:
113+
constbuf=Buffer.from(error.buffer,
114+
error.byteOffset+1,
115+
error.byteLength-1);
116+
returnbuf.toString('utf8');
117+
}
118+
require('assert').fail('This should not happen');
119+
}
120+
121+
module.exports={ serializeError, deserializeError };

‎lib/internal/worker.js‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
constBuffer=require('buffer').Buffer;
43
constEventEmitter=require('events');
54
constassert=require('assert');
65
constpath=require('path');
@@ -17,6 +16,7 @@ const{internalBinding } = require('internal/bootstrap/loaders');
1716
const{ MessagePort, MessageChannel }=internalBinding('messaging');
1817
const{ handle_onclose }=internalBinding('symbols');
1918
const{ clearAsyncIdStack }=require('internal/async_hooks');
19+
const{ serializeError, deserializeError }=require('internal/error-serdes');
2020

2121
util.inherits(MessagePort,EventEmitter);
2222

@@ -453,17 +453,6 @@ function setupChild(evalScript){
453453
}
454454
}
455455

456-
// TODO(addaleax): These can be improved a lot.
457-
functionserializeError(error){
458-
returnBuffer.from(util.inspect(error),'utf8');
459-
}
460-
461-
functiondeserializeError(error){
462-
returnBuffer.from(error.buffer,
463-
error.byteOffset,
464-
error.byteLength).toString('utf8');
465-
}
466-
467456
functionpipeWithoutWarning(source,dest){
468457
constsourceMaxListeners=source._maxListeners;
469458
constdestMaxListeners=dest._maxListeners;

‎node.gyp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
'lib/internal/constants.js',
103103
'lib/internal/encoding.js',
104104
'lib/internal/errors.js',
105+
'lib/internal/error-serdes.js',
105106
'lib/internal/fixed_queue.js',
106107
'lib/internal/freelist.js',
107108
'lib/internal/fs/promises.js',

‎test/parallel/test-error-serdes.js‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
require('../common');
4+
constassert=require('assert');
5+
const{ERR_INVALID_ARG_TYPE}=require('internal/errors').codes;
6+
const{ serializeError, deserializeError }=require('internal/error-serdes');
7+
8+
functioncycle(err){
9+
returndeserializeError(serializeError(err));
10+
}
11+
12+
assert.strictEqual(cycle(0),0);
13+
assert.strictEqual(cycle(-1),-1);
14+
assert.strictEqual(cycle(1.4),1.4);
15+
assert.strictEqual(cycle(null),null);
16+
assert.strictEqual(cycle(undefined),undefined);
17+
assert.strictEqual(cycle('foo'),'foo');
18+
19+
{
20+
consterr=cycle(newError('foo'));
21+
assert(errinstanceofError);
22+
assert.strictEqual(err.name,'Error');
23+
assert.strictEqual(err.message,'foo');
24+
assert(/^Error:foo\n/.test(err.stack));
25+
}
26+
27+
assert.strictEqual(cycle(newRangeError('foo')).name,'RangeError');
28+
assert.strictEqual(cycle(newTypeError('foo')).name,'TypeError');
29+
assert.strictEqual(cycle(newReferenceError('foo')).name,'ReferenceError');
30+
assert.strictEqual(cycle(newURIError('foo')).name,'URIError');
31+
assert.strictEqual(cycle(newEvalError('foo')).name,'EvalError');
32+
assert.strictEqual(cycle(newSyntaxError('foo')).name,'SyntaxError');
33+
34+
classSubErrorextendsError{}
35+
36+
assert.strictEqual(cycle(newSubError('foo')).name,'Error');
37+
38+
assert.deepStrictEqual(cycle({message: 'foo'}),{message: 'foo'});
39+
assert.strictEqual(cycle(Function),'[Function: Function]');
40+
41+
{
42+
consterr=newERR_INVALID_ARG_TYPE('object','Object',42);
43+
assert(/^TypeError\[ERR_INVALID_ARG_TYPE\]:/.test(err));
44+
assert.strictEqual(err.name,'TypeError [ERR_INVALID_ARG_TYPE]');
45+
assert.strictEqual(err.code,'ERR_INVALID_ARG_TYPE');
46+
}

‎test/parallel/test-worker-uncaught-exception-async.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER){
1010
constw=newWorker(__filename);
1111
w.on('message',common.mustNotCall());
1212
w.on('error',common.mustCall((err)=>{
13-
// TODO(addaleax): be more specific here
14-
assert(/foo/.test(err));
13+
assert(/^Error:foo$/.test(err));
1514
}));
1615
}else{
1716
setImmediate(()=>{

‎test/parallel/test-worker-uncaught-exception.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ if (!process.env.HAS_STARTED_WORKER){
1010
constw=newWorker(__filename);
1111
w.on('message',common.mustNotCall());
1212
w.on('error',common.mustCall((err)=>{
13-
// TODO(addaleax): be more specific here
14-
assert(/foo/.test(err));
13+
assert(/^Error:foo$/.test(err));
1514
}));
1615
}else{
1716
thrownewError('foo');

0 commit comments

Comments
(0)