Honor all MessageEventInit members in MessageEvent constructor - #6995
Honor all MessageEventInit members in MessageEvent constructor#6995rexxars wants to merge 2 commits into
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
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.
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.
|
|
||
| kj::OneOf<jsg::JsValue, jsg::Ref<Blob>> getData(jsg::Lock& js); | ||
|
|
||
| kj::Maybe<kj::ArrayPtr<const char>> getOrigin(); |
There was a problem hiding this comment.
This shouldn't need to be changed.
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- If this's origin is an origin, then return the serialization of this's origin.
- If this's origin is null, then return the empty string.
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
f999415 to
b4b570a
Compare
Trying to get the eventsource module working on cloudflare workers revealed an issue (well, multiple - read the last section too):
Same expression on Node returns
'123'. So does every browser.lastEventIdis a member ofMessageEventInitin the HTML spec, alongsideorigin,source, andports.Why it happens
MessageEvent::Initializerdeclared a singledatamember. JSG builds the dictionary conversion from theJSG_STRUCTlist, so everything else was dropped during conversion andMessageEvent::constructorforwarded only that one field. ThelastEventIdfield, its getter and itsJSG_READONLY_INSTANCE_PROPERTYwere all already in place, just unreachable from JS: only the internal C++ constructors thatEventSourceuses could set them.The same conversion dropped
originandsource, and sinceInitializercarried noEventInitmembers,bubbles,cancelableandcomposedwent too.datawas required rather than defaulting tonull, sonew MessageEvent('message')threw aTypeError.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
originis stored verbatim rather than parsed as a URL. AMessageEvent'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'soriginis aUSVString, so this is the string case. Parsing would also break round tripping, since anEventSourceover an opaque origin givesevent.origin === "null", which is not a URL.EventSourceis the one caller that genuinely holds an origin, so it serializes at the call site instead.origindefaults to""rather thannull, per the dictionary. That makes it non-nullable, so the generated type goes fromstring | nulltostring. It is observable to code checkingevent.origin === nullon WebSocket messages, but that only ever sawnullbecause we never populated it, so I don't think anything can reasonably be depending on it.datawidens fromArrayBuffer | stringtoany. That matches both the spec and the getter, which was already typedreadonly data: any, and the runtime always accepted arbitrary values.What this does not touch
isTrustedistruefor script-constructedMessageEvent,CustomEvent, andErrorEvent, but the DOM spec requiresfalse. Only the baseEventgets this right, viaTrusted::NOinEvent::constructor.portsis still dropped. It is aMessageEventInitmember, but we don't support transferringMessagePorts in aMessageEvent, sonew MessageEvent('m', { ports: [port] }).portsis[]. That needs real transfer support rather than just a dictionary member, so I left it out.initMessageEvent()method is still missing.on*handlers causes double firing - already reported in EventTarget is broken and not aligned with spec #6022Questions
Since this is my first contribution here:
isTrusted,portsand the subclassed EventTarget double firing?origindefault change OK without a compatibility flag, or would you rather gate it?