Skip to content

Commit 031e853

Browse files
committed
diagnostics_channel: ensure tracePromise return values are unmodified
Previously, the return value of tracePromise would depend on the presence or absence of subscribers.
1 parent 4612c79 commit 031e853

File tree

4 files changed

+109
-15
lines changed

4 files changed

+109
-15
lines changed

doc/api/diagnostics_channel.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -831,14 +831,15 @@ added:
831831
* `context` {Object} Shared object to correlate trace events through
832832
* `thisArg` {any} The receiver to be used for the function call
833833
* `...args` {any} Optional arguments to pass to the function
834-
* Returns: {Promise} Chained from promise returned by the given function
834+
* Returns: {any} The return value of the given function
835835

836836
Trace a promise-returning function call. This will always produce a
837837
[`start` event][] and [`end` event][] around the synchronous portion of the
838-
function execution, and will produce an [`asyncStart` event][] and
839-
[`asyncEnd` event][] when a promise continuation is reached. It may also
840-
produce an [`error` event][] if the given function throws an error or the
841-
returned promise rejects. This will run the given function using
838+
function execution. If the return value is a {Promise} or [thenable][], it will
839+
also produce an [`asyncStart` event][] and [`asyncEnd` event][] when the
840+
returned promise is fulfilled or rejected. It may also produce an
841+
[`error` event][] if the given function throws an error or the returned promise
842+
rejects. This will run the given function using
842843
[`channel.runStores(context, ...)`][] on the `start` channel which ensures all
843844
events should have any bound stores set to match this trace context.
844845

@@ -1436,3 +1437,4 @@ Emitted when a new thread is created.
14361437
[`process.execve()`]: process.md#processexecvefile-args-env
14371438
[`start` event]: #startevent
14381439
[context loss]: async_context.md#troubleshooting-context-loss
1440+
[thenable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables

lib/diagnostics_channel.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ const {
1010
ObjectDefineProperty,
1111
ObjectGetPrototypeOf,
1212
ObjectSetPrototypeOf,
13-
Promise,
1413
PromisePrototypeThen,
15-
PromiseReject,
1614
PromiseResolve,
1715
ReflectApply,
1816
SafeFinalizationRegistry,
@@ -25,6 +23,9 @@ const {
2523
ERR_INVALID_ARG_TYPE,
2624
},
2725
} = require('internal/errors');
26+
const {
27+
isPromise,
28+
} = require('internal/util/types');
2829
const {
2930
validateFunction,
3031
} = require('internal/validators');
@@ -358,25 +359,28 @@ class TracingChannel {
358359
asyncStart.publish(context);
359360
// TODO: Is there a way to have asyncEnd _after_ the continuation?
360361
asyncEnd.publish(context);
361-
return PromiseReject(err);
362362
}
363363

364364
function resolve(result) {
365365
context.result = result;
366366
asyncStart.publish(context);
367367
// TODO: Is there a way to have asyncEnd _after_ the continuation?
368368
asyncEnd.publish(context);
369-
return result;
370369
}
371370

372371
return start.runStores(context, () => {
373372
try {
374-
let promise = ReflectApply(fn, thisArg, args);
375-
// Convert thenables to native promises
376-
if (!(promise instanceof Promise)) {
377-
promise = PromiseResolve(promise);
373+
const result = ReflectApply(fn, thisArg, args);
374+
375+
// If the result isn't a Promise or thenable, then return it directly.
376+
// Otherwise, register the async tracing callbacks, then return the
377+
// original Promise or thenable.
378+
if (!isPromise(result) && typeof result?.then !== 'function') {
379+
context.result = result;
380+
} else {
381+
PromisePrototypeThen(PromiseResolve(result), resolve, reject);
378382
}
379-
return PromisePrototypeThen(promise, resolve, reject);
383+
return result;
380384
} catch (err) {
381385
context.error = err;
382386
error.publish(context);

test/fixtures/source-map/output/source_map_assert_source_line.snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:
77
*
88
*
99
*
10-
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
10+
at TracingChannel.traceSync (node:diagnostics_channel:329:14)
1111
*
1212
*
1313
*
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const dc = require('diagnostics_channel');
5+
const assert = require('assert');
6+
7+
const kSuccess = Symbol('kSuccess');
8+
const kFailure = Symbol('kFailure');
9+
10+
class DerivedPromise extends Promise {}
11+
12+
const fulfilledThenable = {
13+
then(resolve) {
14+
resolve(kSuccess);
15+
return this;
16+
},
17+
};
18+
const rejectedThenable = {
19+
then(_, reject) {
20+
reject(kFailure);
21+
return this;
22+
},
23+
};
24+
25+
function check(event) {
26+
if (event.expectError) {
27+
assert.strictEqual(event.error, kFailure);
28+
assert.strictEqual(event.result, undefined);
29+
} else {
30+
assert.strictEqual(event.error, undefined);
31+
assert.strictEqual(event.result, kSuccess);
32+
}
33+
}
34+
35+
let i = 0;
36+
37+
function test(expected, { withSubscribers, expectAsync, expectError }) {
38+
const channel = dc.tracingChannel(`test${i++}`);
39+
40+
if (withSubscribers) {
41+
channel.subscribe({
42+
start: common.mustCall(),
43+
end: common.mustCall(),
44+
asyncStart: expectAsync ? common.mustCall(check) : common.mustNotCall(),
45+
asyncEnd: expectAsync ? common.mustCall(check) : common.mustNotCall(),
46+
error: expectError ? common.mustCall(check) : common.mustNotCall(),
47+
});
48+
}
49+
50+
const result = channel.tracePromise(() => expected, { expectError });
51+
assert.strictEqual(result, expected);
52+
}
53+
54+
// Should trace Promises and thenables, and return them unaltered
55+
const testValues = [
56+
[DerivedPromise.resolve(kSuccess), false],
57+
[DerivedPromise.reject(kFailure), true],
58+
[fulfilledThenable, false],
59+
[rejectedThenable, true],
60+
];
61+
for (const [value, expectError] of testValues) {
62+
test(value, { withSubscribers: false });
63+
test(value, { withSubscribers: true, expectAsync: true, expectError });
64+
}
65+
66+
// Should return non-thenable values unaltered, and should not call async handlers
67+
test(kSuccess, { withSubscribers: false });
68+
test(kSuccess, { withSubscribers: true, expectAsync: false, expectError: false });
69+
70+
// Should throw synchronous exceptions unaltered
71+
{
72+
const channel = dc.tracingChannel(`test${i++}`);
73+
74+
const error = new Error('Synchronous exception');
75+
const throwError = () => { throw error; };
76+
77+
assert.throws(() => channel.tracePromise(throwError), error);
78+
79+
channel.subscribe({
80+
start: common.mustCall(),
81+
end: common.mustCall(),
82+
asyncStart: common.mustNotCall(),
83+
asyncEnd: common.mustNotCall(),
84+
error: common.mustCall(),
85+
});
86+
87+
assert.throws(() => channel.tracePromise(throwError), error);
88+
}

0 commit comments

Comments
 (0)