Skip to content

URL exposes its twelve components as own enumerable instance properties; node keeps them as accessors on URL.prototype (Object.keys, for...in, spread all diverge) #10823

Closed

Description

@proggeramlug

URL stores its twelve components as own enumerable properties on the instance. Node keeps all of
them as accessors on URL.prototype and gives the instance zero own properties, so every operation
that enumerates an object disagrees.

Repro

const u: any = new URL("https://e.com/p?q=1");
console.log("keys    ", JSON.stringify(Object.keys(u)));
console.log("ownNames", JSON.stringify(Object.getOwnPropertyNames(u)));
node 26.8.1 perry v0.5.1618
Object.keys(u) [] ["href","protocol","host","hostname","port","pathname","search","hash","origin","searchParams","username","password"]
Object.getOwnPropertyNames(u) [] the same twelve
Object.prototype.toString.call(u) [object URL] [object URL] (matches)
JSON.stringify(u) "https://e.com/p?q=1" "https://e.com/p?q=1" (matches, via toJSON)

Anything that walks own properties therefore diverges: for...in, object spread {...u},
Object.assign({}, u), Object.entries, a deepStrictEqual against a plain object, and any
serializer that enumerates rather than calling toJSON.

The brand and JSON.stringify happen to match, so a test that only checks those passes — which is
probably why this has not been noticed.

Cause

URL is one of the web classes that is already a real ObjectHeader rather than a registry handle,
but its components are written as ordinary instance fields rather than installed as accessors on a
URL.prototype. Related, from crates/perry-runtime/src/object/to_string_tag.rs:18-32, the same
family of classes has a second half of this gap:

AbortController / AbortSignal / EventTarget / Event / CustomEvent (real ObjectHeaders
whose instances are never linked to their .prototype object via object_static_prototype, so
the generic own/inherited-property walk in object_to_string_tag_property can never reach a tag
installed there).

So for this group the prototype objects exist but instances are not linked to them, and state that
belongs on the prototype as accessors is kept on the instance instead.

Fix

Install the components as accessors on a real URL.prototype and link instances to it with
object_static_prototype, leaving the instance with no own properties. The runtime already has the
pieces: class_registry/prototype_objects.rs ensure_function_prototype_object() builds a real
prototype and mirrors methods onto it, object/iterator_prototypes.rs build_family_proto() is a
working template, and ObjectMeta.prototype is a direct write.

Worth checking the same way: AbortController, AbortSignal, Event, CustomEvent,
DOMException, EventTarget, URLSearchParams.

Why this is being filed now

It is the shape a planned change must not reproduce. #10821 proposes turning the thirteen native
handle families into ordinary objects to give them identity and put them on the single property-read
path. Done naively — state in instance fields — that would give thirteen more classes exactly this
bug. Done correctly — state behind prototype accessors, instance with zero own keys — it matches
node, which is also what node itself does for all of these classes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions