File
Blob: src/workerd/jsg/dom-exception.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "dom-exception.h" |
| 6 | |
| 7 | #include "jsvalue.h" |
| 8 | #include "ser.h" |
| 9 | |
| 10 | #include <workerd/jsg/memory.h> |
| 11 | |
| 12 | #include <kj/string.h> |
| 13 | |
| 14 | #include <map> |
| 15 | |
| 16 | namespace workerd::jsg { |
| 17 | |
| 18 | Ref<DOMException> DOMException::constructor(const v8::FunctionCallbackInfo<v8::Value>& args, |
| 19 | Optional<kj::String> message, |
| 20 | Optional<kj::String> name) { |
| 21 | Lock& js = Lock::from(args.GetIsolate()); |
| 22 | kj::String errMessage = kj::mv(message).orDefault([&] { return kj::String(); }); |
| 23 | |
| 24 | // V8 gives Error objects a non-standard (but widely known) `stack` property, and Web IDL |
| 25 | // requires that DOMException get any non-standard properties that Error gets. Chrome honors |
| 26 | // this requirement only for runtime-generated DOMExceptions -- script-generated DOMExceptions |
| 27 | // don't get `stack`, even though script-generated Errors do. It's more convenient and, IMO, |
| 28 | // more conformant to just give all DOMExceptions a `stack` property. |
| 29 | jsg::check(v8::Exception::CaptureStackTrace(js.v8Context(), args.This())); |
| 30 | |
| 31 | // This part is a bit of a hack. By default, the various properties on JavaScript errors |
| 32 | // are not enumerable. However, our implementation of DOMException has always defined |
| 33 | // them as enumerable, which means just setting the stack above would be a breaking change. |
| 34 | // To maintain backwards compat we have to define the stack as enumerable here. |
| 35 | v8::PropertyDescriptor prop; |
| 36 | prop.set_enumerable(true); |
| 37 | v8::Local<v8::String> stackName = js.str("stack"_kjc); |
| 38 | jsg::check(args.This()->DefineProperty(js.v8Context(), stackName, prop)); |
| 39 | |
| 40 | return js.alloc<DOMException>( |
| 41 | kj::mv(errMessage), kj::mv(name).orDefault([] { return kj::str("Error"); })); |
| 42 | } |
| 43 | |
| 44 | kj::StringPtr DOMException::getName() const { |
| 45 | return name; |
| 46 | } |
| 47 | |
| 48 | kj::StringPtr DOMException::getMessage() const { |
| 49 | return message; |
| 50 | } |
| 51 | |
| 52 | int DOMException::getCode() const { |
| 53 | static const std::map<kj::StringPtr, int> legacyCodes{ |
| 54 | #define MAP_ENTRY(name, code, friendlyName) {friendlyName, code}, |
| 55 | JSG_DOM_EXCEPTION_FOR_EACH_ERROR_NAME(MAP_ENTRY) |
| 56 | #undef MAP_ENTRY |
| 57 | }; |
| 58 | auto code = legacyCodes.find(name); |
| 59 | if (code != legacyCodes.end()) { |
| 60 | return code->second; |
| 61 | } |
| 62 | return 0; |
| 63 | } |
| 64 | |
| 65 | void DOMException::serialize(jsg::Lock& js, jsg::Serializer& serializer) { |
| 66 | serializer.writeLengthDelimited(name); |
| 67 | serializer.writeLengthDelimited(message); |
| 68 | |
| 69 | // It's a bit unfortunate that the stack here ends up also including the name and message |
| 70 | // so we end up duplicating some of the information here, but that's OK. It's better to |
| 71 | // keep this implementation simple rather than to implement any kind of deduplication. |
| 72 | KJ_IF_SOME(stack, this->stack.get(js, "stack")) { |
| 73 | serializer.writeLengthDelimited(stack); |
| 74 | } else { |
| 75 | // This branch shouldn't really be taken in the typical case. It's only here |
| 76 | // to handle the case where the stack property could not be unwrapped for some |
| 77 | // reason. We don't need to treat it as an error case, just set the stack to |
| 78 | // the empty string and move on. |
| 79 | serializer.writeLengthDelimited(""_kj); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | jsg::Ref<DOMException> DOMException::deserialize( |
| 84 | jsg::Lock& js, uint tag, jsg::Deserializer& deserializer) { |
| 85 | switch (tag) { |
| 86 | case SERIALIZATION_TAG_V2: { |
| 87 | kj::String name = deserializer.readLengthDelimitedString(); |
| 88 | kj::String message = deserializer.readLengthDelimitedString(); |
| 89 | kj::String stack = deserializer.readLengthDelimitedString(); |
| 90 | return js.domException(kj::mv(name), kj::mv(message), kj::mv(stack)); |
| 91 | } |
| 92 | case SERIALIZATION_TAG: { |
| 93 | // This is the original serialization of DOMException. It was only |
| 94 | // used for a very short period of time (a matter of weeks) but there's |
| 95 | // still a remote chance that someone might use it in some persisted state |
| 96 | // somewhere. So let's go ahead and support it. |
| 97 | kj::String name = deserializer.readLengthDelimitedString(); |
| 98 | auto errorForStack = KJ_UNWRAP_OR(deserializer.readValue(js).tryCast<JsObject>(), { |
| 99 | JSG_FAIL_REQUIRE( |
| 100 | DOMDataCloneError, "Deserialization failed: DOMException payload is not an object"); |
| 101 | }); |
| 102 | auto messageJs = KJ_UNWRAP_OR(errorForStack.get(js, "message"_kj).tryCast<JsString>(), { |
| 103 | JSG_FAIL_REQUIRE( |
| 104 | DOMDataCloneError, "Deserialization failed: DOMException message is not a string"); |
| 105 | }); |
| 106 | auto stackJs = KJ_UNWRAP_OR(errorForStack.get(js, "stack").tryCast<JsString>(), { |
| 107 | JSG_FAIL_REQUIRE( |
| 108 | DOMDataCloneError, "Deserialization failed: DOMException stack is not a string"); |
| 109 | }); |
| 110 | kj::String message = messageJs.toString(js); |
| 111 | kj::String stack = stackJs.toString(js); |
| 112 | return js.domException(kj::mv(message), kj::mv(name), kj::mv(stack)); |
| 113 | } |
| 114 | default: |
| 115 | KJ_UNREACHABLE; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | } // namespace workerd::jsg |