workerd icon indicating copy to clipboard operation
workerd copied to clipboard

logging importable exports shows an empty object

Open threepointone opened this issue 3 months ago • 12 comments

What versions & operating system are you using?

"@cloudflare/vite-plugin": "^1.15.3",
"@cloudflare/workers-types": "^4.20251127.0",
"typescript": "^5.9.3",
"vite": "^7.2.4",
"wrangler": "^4.51.0"

Please provide a link to a minimal reproduction

https://github.com/threepointone/repro-vite-exports-bug

Describe the Bug

importable exports aren't logged with the module's exports (as specified in https://github.com/cloudflare/workerd/pull/5516). ctx.exports are ok and work as expected.

Please provide any relevant error logs

No response

threepointone avatar Nov 27 '25 12:11 threepointone

Looks like this is actually an issue when logging the exports object. Since it's a Proxy, it doesn't enumerate the members and logs an empty object. The properties are actually present.

threepointone avatar Nov 27 '25 13:11 threepointone

Proxies can implement enumeration. RPC proxies don't because they actually don't know what methods the other side implements. But, the exports (and env) proxies ought to implement enumeration.

@anonrig

kentonv avatar Nov 28 '25 18:11 kentonv

https://github.com/cloudflare/workerd/pull/5608 doesn't fix this issue. Importable exports are still logged as an empty object.

jamesopstad avatar Dec 03 '25 13:12 jamesopstad

@jasnell any idea what's happening here?

anonrig avatar Dec 04 '25 14:12 anonrig

I've opened #5647 to revert the changes from #5608 but keep the tests -- which still pass, because the change didn't actually do anything (the properties are already enumerable).

kentonv avatar Dec 05 '25 13:12 kentonv

Testing the actual bug a little bit, I find:

  1. This applies to both importable env and exports. Presumably it's not new.
  2. It only seems to affect console.log(). JSON.stringify(), in contrast, sees all the members. This is very odd and suggests the problem is specific to console.log, not a general problem with the proxy?
  3. toString() returns [object Object], so it's not an issue with that.

I am not sure where console.log() behavior is implemented exactly, but I think this is something we have customized? Perhaps the bug is there?

kentonv avatar Dec 05 '25 13:12 kentonv

In workerd, the console.log output ends up being routed through the nodejs_compat util.inspect and util.format methods, which does include some limited support for inspecting proxies. You'll find tho, that it doesn't really handle proxies all that well in the default case. Even in Node.js, the following ends up just printing {}:

const p = new Proxy({}, {
  ownKeys() { return ['a']; },
  getOwnPropertyDescriptor() {
    return { value: 1, enumerable: true, configurable: true, writable: true };
  },
  has() { return true; },
  get() { return 1; },
});
console.log(p);  // In Node.js... prints '{}'

In Node.js, the util.format implements a %j flag (e.g. console.log('%j', p)) that causes it to start printing mostly correctly... only because it JSON serializes the object and prints the resulting string. We haven't implemented the %j flag so that doesn't work in workerd yet.

So the proxy itself is fine. What you're seeing here is a limitation of the Node.js util.format/util.inspect when interacting with proxies.

jasnell avatar Dec 05 '25 15:12 jasnell

I've opened an issue in Node.js about this: https://github.com/nodejs/node/issues/60964

The correct fix here would be to update the implementation of util.inspect/util.format to handle proxies more effectively.

jasnell avatar Dec 05 '25 16:12 jasnell

There's apparently a showProxy option for this:

https://github.com/cloudflare/workerd/blob/eb0bfa22376f6a20dd0dac08a077a022316e5c19/src/node/internal/internal_inspect.ts#L152-L156

Otherwise proxies are intentionally hidden:

https://github.com/cloudflare/workerd/blob/eb0bfa22376f6a20dd0dac08a077a022316e5c19/src/node/internal/internal_inspect.ts#L1128-L1139

It... really bugs me that there is a built-in function getProxyDetails() which permits unwrapping proxies. I realize it's in internal utils that (hopefully) can't be imported by application code, but there's a reason why there's no language built-in for this, and it bothers me to have a back door exposed to JS at all...

kentonv avatar Dec 08 '25 17:12 kentonv

The showProxy option shows details about the proxy itself and not the proxied object. It turns out that the behavior here is intentional. The util.inspect/util/format logic is avoiding triggering proxy traps that potentially have side effects, which makes sense. Discussing within Node.js (https://github.com/nodejs/node/issues/60964) how we can iterate on this.

The getProxyDetails is a carry over from node.js for debugging and is there for node.js compat. It is not exposed to user code

jasnell avatar Dec 08 '25 17:12 jasnell

Ok, in the interim.. a good solution here would be to have these Proxy objects implement the Node.js util.inspect.custom protocol. The Proxy objects themselves can have add an implementation of the custom inspect function that should be called by the implementation.

For instance,

const p = new Proxy({}, { ownKeys() { return ['a'] } });
console.log(p);  // {}
p[util.inspect.custom] = () => 1;
console.log(p);  // 1

Docs: https://nodejs.org/docs/latest/api/util.html#utilinspectcustom

Update: eh, actually, that only works for exports, not for env. Will have to think about it some more.

jasnell avatar Dec 08 '25 18:12 jasnell

Update: Node.js will likely update it's handling here to output Proxy {} rather than just {} so at least it's a bit easier to understand the reasoning behing the empty output. https://github.com/nodejs/node/pull/61029

jasnell avatar Dec 12 '25 16:12 jasnell