Skip to content

Honor all MessageEventInit members in MessageEvent constructor - #6995

Open
rexxars wants to merge 2 commits into
cloudflare:mainfrom
rexxars:fix/message-event-init
Open

Honor all MessageEventInit members in MessageEvent constructor#6995
rexxars wants to merge 2 commits into
cloudflare:mainfrom
rexxars:fix/message-event-init

Conversation

@rexxars

@rexxars rexxars commented Aug 13, 2026

Copy link
Copy Markdown

Trying to get the eventsource module working on cloudflare workers revealed an issue (well, multiple - read the last section too):

new MessageEvent('message', { data: 'x', lastEventId: '123' }).lastEventId;
// ''

Same expression on Node returns '123'. So does every browser. lastEventId is a member of MessageEventInit in the HTML spec, alongside origin, source, and ports.

Why it happens

MessageEvent::Initializer declared a single data member. JSG builds the dictionary conversion from the JSG_STRUCT list, so everything else was dropped during conversion and MessageEvent::constructor forwarded only that one field. The lastEventId field, its getter and its JSG_READONLY_INSTANCE_PROPERTY were all already in place, just unreachable from JS: only the internal C++ constructors that EventSource uses could set them.

The same conversion dropped origin and source, and since Initializer carried no EventInit members, bubbles, cancelable and composed went too. data was required rather than defaulting to null, so new MessageEvent('message') threw a TypeError.

For reference, the spec dictionary is:

dictionary MessageEventInit : EventInit {
  any data = null;
  USVString origin = "";
  DOMString lastEventId = "";
  MessageEventSource? source = null;
  sequence<MessagePort> ports = [];
};

Notes on a few choices

origin is stored verbatim rather than parsed as a URL. A MessageEvent's origin is "an origin, a string, or null", and the getter only serializes when it holds an actual origin; a string is returned as given. MessageEventInit's origin is a USVString, so this is the string case. Parsing would also break round tripping, since an EventSource over an opaque origin gives event.origin === "null", which is not a URL. EventSource is the one caller that genuinely holds an origin, so it serializes at the call site instead.

origin defaults to "" rather than null, per the dictionary. That makes it non-nullable, so the generated type goes from string | null to string. It is observable to code checking event.origin === null on WebSocket messages, but that only ever saw null because we never populated it, so I don't think anything can reasonably be depending on it.

data widens from ArrayBuffer | string to any. That matches both the spec and the getter, which was already typed readonly data: any, and the runtime always accepted arbitrary values.

What this does not touch

  • isTrusted is true for script-constructed MessageEvent, CustomEvent, and ErrorEvent, but the DOM spec requires false. Only the base Event gets this right, via Trusted::NO in Event::constructor.
  • ports is still dropped. It is a MessageEventInit member, but we don't support transferring MessagePorts in a MessageEvent, so new MessageEvent('m', { ports: [port] }).ports is []. That needs real transfer support rather than just a dictionary member, so I left it out.
  • The legacy initMessageEvent() method is still missing.
  • Subclassing EventTarget and adding on* handlers causes double firing - already reported in EventTarget is broken and not aligned with spec #6022

Questions

Since this is my first contribution here:

  • Would you prefer a wider fix that also addresses isTrusted, ports and the subclassed EventTarget double firing?
  • Is the origin default change OK without a compatibility flag, or would you rather gate it?

@rexxars
rexxars requested review from a team as code owners August 13, 2026 20:32
@rexxars
rexxars requested a review from edmundhung August 13, 2026 20:32
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@rexxars

rexxars commented Aug 13, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Aug 13, 2026
rexxars added a commit to sanity-io/client that referenced this pull request Aug 14, 2026
Record cloudflare/workerd#6995 where someone will look for it: the comment on
the workerd CI job and the dependency note in CONTRIBUTING. The upstream PR
honors all `MessageEventInit` members, so `lastEventId` was one symptom of the
whole init dict being dropped rather than a single missing member; the comment
now says that.

Once the upstream fix ships, the `eventsource` floor can be revisited, and the
workerd job is the check that proves it.
rexxars added a commit to sanity-io/client that referenced this pull request Aug 15, 2026
Record cloudflare/workerd#6995 where someone will look for it: the comment on
the workerd CI job and the dependency note in CONTRIBUTING. The upstream PR
honors all `MessageEventInit` members, so `lastEventId` was one symptom of the
whole init dict being dropped rather than a single missing member; the comment
now says that.

Once the upstream fix ships, the `eventsource` floor can be revisited, and the
workerd job is the check that proves it.
Comment thread src/workerd/api/events.h
Comment thread src/workerd/api/events.h

kj::OneOf<jsg::JsValue, jsg::Ref<Blob>> getData(jsg::Lock& js);

kj::Maybe<kj::ArrayPtr<const char>> getOrigin();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't need to be changed.

@rexxars rexxars Aug 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The kj::Array to kj::String change is gone, but getOrigin() still changes, because origin now defaults to an empty string rather than null per the spec, so it can never be null and the kj::Maybe would generate string | null for something that is always a string.


return js.alloc<MessageEvent>(js, kj::mv(type), kj::mv(data),
kj::mv(initializer.lastEventId).orDefault(kj::String()), kj::mv(initializer.source),
kj::mv(origin),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of just accepting any origin string provided, this really ought to validate that it parses as a jsg::Url. Doing so means you don't have to change the type for the origin itself.

@rexxars rexxars Aug 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f999415. origin is now parsed with jsg::Url::tryParse and a TypeError is thrown if it does not parse, so maybeOrigin stays a kj::Array<const char> holding a real serialized origin. That reverts the two type changes you flagged, and EventSource::notifyMessages goes back to passing the jsg::Url&.

Two consequences which I think are fine but are deviations from the spec:

  • { origin: "https://example.org/path" } stores "https://example.org", i.e. the value is normalized rather than kept verbatim.
  • { origin: "null" } (the serialization of an opaque origin) now throws. Browsers accept it.

Happy to relax either if you would rather match the spec exactly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having thought about this some more, I don't think it makes sense to differ from the spec here. I'm a little surprised by how lax the spec seems to be about what it allows, but differing from the spec means differing behavior between runtimes, which is what I was trying to avoid with this PR in the first place. I'll rethink.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I initially went with validation as suggested, but reconsidered. As noted above, I don't think it makes sense to differ from the spec here. Not that it makes a difference, but I agree that it would have been better if the spec did validate, and did use only the actual "origin" of a URL. But the spec only says that a MessageEvent's origin is "an origin, a string, or null", and the getter is:

  1. If this's origin is an origin, then return the serialization of this's origin.
  2. If this's origin is null, then return the empty string.
  3. Return this's origin.

The practical problem with parsing is that it breaks round tripping on values our own serializer produces. An EventSource over an opaque origin gives event.origin === "null", and new MessageEvent('message', { origin: ev.origin }) then throws, since "null" is not a URL. Same for "", which is the dictionary default.

So the latest commit stores the string as-is, and moves the serialization into EventSource::notifyMessages, which is the one place that actually holds an origin.

If you disagree with the approach and want to deviate from the spec, let me know.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would then try parsing, and only if it fails pass through the string content (as the kj::Array<const char>, but if it succeeds, passing it through using the jsg::Url.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't that go against the spec? If we pass a string such as https://example.org:443/path to origin, it would parse, but it would normalize it to https://example.org, so it wouldn't preserve what the author gave it?

@rexxars
rexxars force-pushed the fix/message-event-init branch from f999415 to b4b570a Compare August 15, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants