Repository navigation
Throw DOMExceptions from Rust JSG - #7951
bartlomieju wants to merge 7 commits into
Conversation
Rust JSG offered DOM error types such as AbortError and NotSupportedError but threw them all as a plain Error, and could not express DOMException(SyntaxError) because SyntaxError meant the JS class. Rename the DOM variants to mirror the C++ JSG_REQUIRE error types (DOMSyntaxError, DOMAbortError, ...), throw real DOMExceptions for them, and recognize DOMExceptions in tunneled KJ errors and in FromJS for jsg::Error. No shipped Rust API throws these types, so observable JS behaviour is unchanged.
…mexception # Conflicts: # src/rust/jsg-test/tests/mod.rs # src/rust/jsg/README.md # src/rust/jsg/ffi.c++ # src/rust/jsg/lib.rs
Creating a DOMException clears V8's pending exception, so a DOM-typed error returned after a termination replaced it with an exception that JavaScript could catch. Throwing now does nothing while terminating, and a failed creation lets V8's pending exception propagate instead of logging an internal error. DOMException names without a DOM-prefixed type are kept, and internal DOM errors throw a DOMException with the redacted message, as C++ JSG does.
piscisaureus
left a comment
There was a problem hiding this comment.
Direction is right. The two new shims add jsg::Lock/jsg::Ref, which rule 3 normally forbids, but I'm OK with that as an allowlisted exception: DOMException is a C++ JSG resource and Rust has to produce the same class.
Please rebase onto main (#7965), bump the allowlist explicitly, and keep the TODO pointing at moving DOMException out of C++ JSG. Expect a conflict with #7961 in FromJS for Error. Nit: log_internal_error builds the "internal error; reference = " text by hand instead of using renderInternalError.
| /// Each DOM-prefixed type with the name and legacy `code` the thrown | ||
| /// `DOMException` has (`src/workerd/jsg/dom-exception.h`). | ||
| #[rustfmt::skip] | ||
| const DOM_TYPES: [(ExceptionType, &str, u16); 13] = [ |
There was a problem hiding this comment.
We should likely just move DOMException to per-isolate TS. On the native side we just need a function to create a DE instance.
|
Pausing this for now, will wait until per-isolate autogate is gone and then try to do minimal V8 bindings + TS impl |
|
A follow-up for this PR: the Rust port of When a fallback produces a value that can't be serialized, C++
The port's trace parity test ( |
…mexception # Conflicts: # src/rust/jsg-test/ffi.c++ # src/rust/jsg-test/ffi.h # src/rust/jsg-test/lib.rs # src/rust/jsg/ffi.c++ # src/rust/jsg/ffi.h # src/rust/jsg/lib.rs # src/rust/jsg/v8.rs
Every caller only wanted the JS handle, and the filesystem API took a TypeHandler<Ref<DOMException>> on 16 methods just to wrap the Ref. With a JsObject the DOMException no longer has to be a C++ JSG resource, which lets it move to the per-isolate TypeScript bootstrap. No behaviour change.
A DOMException now carries the name and message it was created with in V8 private properties. Native code recognizes one by them and creates one as a plain object over the context's DOMException prototype, held in a new context data slot, with the own `code`, `name`, `message` and `stack` properties a DOMException has always had. Neither runs JavaScript, and neither needs the C++ JSG resource, so the TypeScript class the per-isolate bootstrap will install can share them. The serializers write a DOMException by its brand. Rust JSG creates and recognizes DOMExceptions through the same V8-only functions, so its shims no longer use jsg::Lock or jsg::Ref, and the allowlist is back to its counts on main. Only an object's own brand counts. C++ JSG also unwrapped an object that had a DOMException on its prototype chain, such as `Object.create(new DOMException())`; nothing depends on that. kj::str() of a natively created DOMException now gives "Name: message" rather than an empty string. MemoryCache.read() relied on the empty string to report a value that cannot be serialized as "failed to serialize <type>", so it now checks for a DOMException explicitly.
The per-isolate bootstrap replaces the C++ DOMException with a TypeScript one (src/per_isolate/dom-exception.ts), and hands native code its prototype, so the DOMExceptions native code and Rust JSG create are instances of it. Its constructor brands an instance as native code does, through a new utils.initDOMException(), so both kinds are recognized alike. The class copies the C++ class's shape: the global lives on the global object's prototype with the same attributes; instances have own read-only `code`, `name` and `message` and an enumerable `stack` in that order; the legacy constants are on the constructor and the prototype; and the attributes that compatibility flags choose (set_tostring_tag, immutable_api_prototypes, spec_compliant_property_attributes) are copied from the C++ class rather than recomputed. The stack leaves out the constructor's own frame. dom-exception-test pins the shape, and passes against the C++ class too. Compatibility: this changes the standard API surface in one way, which we accept: Function.prototype.toString() of DOMException shows its source rather than `[native code]`. It is ungated. The webfs bootstrap recognizes a DOMException by its brand rather than with instanceof, and the primordials no longer capture DOMException.
|
The generated output of Full Type Diff |
|
Not ready for review yet, I'm still iterating on this one |
|
|
||
| // The message and name are converted as Web IDL converts a DOMString, which a template literal | ||
| // does too: a Symbol throws a TypeError. | ||
| function DOMException(this: object, ...args: unknown[]): void { |
There was a problem hiding this comment.
I know you mentioned that you're still iterating.. but I'm curious... why a function and not a class DOMException?
Rust JSG offered DOM error types such as AbortError and NotSupportedError but threw them all as a plain Error, and could not express DOMException(SyntaxError) because SyntaxError meant the JS class. Rename the DOM variants to mirror the C++ JSG_REQUIRE error types (DOMSyntaxError, DOMAbortError, ...), throw real DOMExceptions for them, and recognize DOMExceptions in tunneled KJ errors and in FromJS for jsg::Error.
To do that without C++ JSG in Rust JSG's shims, the global
DOMExceptionmoves to the per-isolate TypeScript bootstrap, and native code creates and recognizes DOMExceptions with V8 alone.Why land this before any Rust API needs it:
new_abort_error,jsg_require!(…, NotSupportedError, …)) that silently throw a plainError, a trap for every API ported to Rust JSG.jsg.DOMException(SyntaxError)from C++ decodes in Rust to the JSSyntaxError, the wrong type.AbortSignal. The Rust port ofMemoryCache.read()needsDOMDataCloneErrorfor parity (#6465048856).Commits
origin/main: resolves the conflicts with Propagate JavaScript exceptions through Rust JSG function calls #7954/Keep what JavaScript throws during Rust JSG coercions #7961/Write down and check the Rust JSG safety rules #7965 and teaches main's newLock::exception()about DOM-named internal errors. This commit temporarily bumps the allowlist so it passes on its own; commit 4 removes the bump.JsObjectfromjsg::Lock::domException(): every caller only wanted the handle; drops the 16TypeHandler<Ref<DOMException>>parameters from the filesystem API. No behaviour change.tryGetDOMExceptionFields()) and creates one (newDOMException()) as a plain object over the context's DOMException prototype, held in a newContextDataSlot, without running JavaScript (so it still works underDisallowJavascriptExecution, e.g. in the deserializer). The serializers,ExceptionWrapper,reportErrorand Rust JSG all use the brand. Rust JSG's shims no longer usejsg::Lock/jsg::Ref; the allowlist is back to main's counts.src/per_isolate/dom-exception.tsreplaces the C++ class as the global, and hands native code its prototype, so runtime-created DOMExceptions are instances of it. It copies the C++ class's shape exactly, including the flag-dependent attributes (set_tostring_tag,immutable_api_prototypes, constant attributes), which it reads off the C++ class rather than recomputing. The C++ class remains for isolates without a bootstrap (JSG and Rust JSG tests) and for the deserializer registration of tags 7/8.Behaviour
Behaviour that matches C++ JSG:
Lock::throw_exceptionthrows nothing while execution is terminating, andError::to_localrefuses to create a DOMException then. If creating one fails with an exception pending, that exception propagates instead of an internal error.Error::new_dom_exception(name, message)accepts any DOMException name, asjsg::Lock::domException()does. A name without a DOM-prefixed type, such as the legacyIndexSizeError, keeps its name (Error::dom_exception_name()) throughFromJS, tunneled errors andDisplay.jsg-internal.DOMException(<Name>)is thrown as a DOMException with that name and the redactedinternal error; reference = <id>message, logged asmakeInternalError()logs it.src/workerd/api/tests/dom-exception-testpins the shape and passes against the C++ class too.Changes:
Function.prototype.toString()ofDOMExceptionshows its source instead of[native code]. This is the only standard-API-surface difference found.kj::str()of a natively created DOMException now givesName: messageinstead of an empty string.MemoryCache.read()relied on the empty string to report an unserializable value as "failed to serialize ", so it now checks for a DOMException explicitly. Itsfallback_errorspan tag for that case becomesDataCloneError: failed to serialize <type>instead of""— relevant for the parity test of the RustMemoryCache.read()port.Object.create(new DOMException())); nothing depends on that.Error(or a JSSyntaxErrorforDOMException(SyntaxError)). No shipped API reaches this:UrlUtilandDnsUtilonly receive values their TypeScript wrappers have already converted.Fast API callbacks are unaffected: a method returning
Resulthas no fast callback, so a fast callback only throwsTypeErrors from its receiver and argument checks (now checked with adebug_assert!).Not covered