File
Blob: src/workerd/jsg/promise.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 "async-context.h" |
| 6 | #include "jsg.h" |
| 7 | |
| 8 | namespace workerd::jsg { |
| 9 | |
| 10 | namespace { |
| 11 | template <typename T> |
| 12 | v8::Local<T> getLocal(v8::Isolate* isolate, v8::Global<T>& global) { |
| 13 | if (!global.IsEmpty()) { |
| 14 | return global.Get(isolate); |
| 15 | } |
| 16 | return v8::Local<T>(); |
| 17 | }; |
| 18 | |
| 19 | kj::Maybe<Ref<AsyncContextFrame>> getFrameRef(jsg::Lock& js) { |
| 20 | return AsyncContextFrame::current(js).map( |
| 21 | [](AsyncContextFrame& frame) -> Ref<AsyncContextFrame> { return frame.addRef(); }); |
| 22 | } |
| 23 | |
| 24 | kj::Maybe<AsyncContextFrame&> tryGetFrame(kj::Maybe<Ref<AsyncContextFrame>>& maybeFrame) { |
| 25 | return maybeFrame.map( |
| 26 | [](Ref<AsyncContextFrame>& frame) -> AsyncContextFrame& { return *frame.get(); }); |
| 27 | } |
| 28 | } // namespace |
| 29 | |
| 30 | UnhandledRejectionHandler::UnhandledRejection::UnhandledRejection(jsg::Lock& js, |
| 31 | jsg::V8Ref<v8::Promise> promise, |
| 32 | jsg::Value value, |
| 33 | v8::Local<v8::Message> message) |
| 34 | : hash(kj::hashCode(promise.getHandle(js)->GetIdentityHash())), |
| 35 | promise(js.v8Isolate, promise.getHandle(js)), |
| 36 | value(js.v8Isolate, value.getHandle(js)), |
| 37 | message(js.v8Isolate, message), |
| 38 | asyncContextFrame(getFrameRef(js)) {} |
| 39 | |
| 40 | void UnhandledRejectionHandler::report( |
| 41 | Lock& js, v8::PromiseRejectEvent event, jsg::V8Ref<v8::Promise> promise, jsg::Value value) { |
| 42 | js.tryCatch([&] { |
| 43 | switch (event) { |
| 44 | case v8::PromiseRejectEvent::kPromiseRejectWithNoHandler: { |
| 45 | rejectedWithNoHandler(js, kj::mv(promise), kj::mv(value)); |
| 46 | return; |
| 47 | } |
| 48 | case v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject: { |
| 49 | handledAfterRejection(js, kj::mv(promise)); |
| 50 | return; |
| 51 | } |
| 52 | case v8::PromiseRejectEvent::kPromiseRejectAfterResolved: { |
| 53 | break; |
| 54 | } |
| 55 | case v8::PromiseRejectEvent::kPromiseResolveAfterResolved: { |
| 56 | break; |
| 57 | } |
| 58 | } |
| 59 | }, [&](Value exception) { |
| 60 | // Exceptions here should be rare but possible. Any errors that occur |
| 61 | // here are likely fatal to the worker. This handling helps us avoid |
| 62 | // crashing. We'll log the error hand continue. |
| 63 | if (js.areWarningsLogged()) { |
| 64 | js.logWarning(kj::str("There was an error while reporting an unhandled promise rejection: ", |
| 65 | exception.getHandle(js))); |
| 66 | } |
| 67 | }); |
| 68 | } |
| 69 | |
| 70 | UnhandledRejectionHandler::UnhandledRejection::~UnhandledRejection() { |
| 71 | if (promise.IsWeak()) { |
| 72 | promise.ClearWeak<void>(); |
| 73 | } |
| 74 | if (value.IsWeak()) { |
| 75 | value.ClearWeak<void>(); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | void UnhandledRejectionHandler::clear() { |
| 80 | warnedRejections.clear(); |
| 81 | unhandledRejections.clear(); |
| 82 | } |
| 83 | |
| 84 | void UnhandledRejectionHandler::rejectedWithNoHandler( |
| 85 | jsg::Lock& js, jsg::V8Ref<v8::Promise> promise, jsg::V8Ref<v8::Value> value) { |
| 86 | auto message = v8::Exception::CreateMessage(js.v8Isolate, value.getHandle(js)); |
| 87 | |
| 88 | // It's not yet clear under what conditions it happens, but this can be called |
| 89 | // twice with the same promise. It really shouldn't happen in the regular cases |
| 90 | // but we address the edge case by using upsert and just replacing the existing |
| 91 | // value and message when it does. |
| 92 | |
| 93 | unhandledRejections.upsert( |
| 94 | UnhandledRejection(js, kj::mv(promise), kj::mv(value), kj::mv(message)), |
| 95 | [&](UnhandledRejection& existing, UnhandledRejection&& replacement) { |
| 96 | // Replacing the promise here is defensive, since they have the same hash |
| 97 | // it *should* be the same promise, but let's be sure. We don't need to |
| 98 | // assert here because the book keeping on this is not critical. |
| 99 | existing = kj::mv(replacement); |
| 100 | }); |
| 101 | |
| 102 | ensureProcessingWarnings(js); |
| 103 | } |
| 104 | |
| 105 | void UnhandledRejectionHandler::handledAfterRejection( |
| 106 | jsg::Lock& js, jsg::V8Ref<v8::Promise> promise) { |
| 107 | // If an unhandled rejection is found in the table, then all we need to do is erase it. |
| 108 | // If it's not found, then we'll skip on to the next step of determining if we've already |
| 109 | // emitted an unhandled rejection warning about this promise to determine if we need to |
| 110 | // emit another warning indicating that it's been handled. |
| 111 | KJ_DEFER(ensureProcessingWarnings(js)); |
| 112 | |
| 113 | HashedPromise key(promise.getHandle(js)); |
| 114 | |
| 115 | if (unhandledRejections.eraseMatch(key)) { |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | KJ_IF_SOME(item, warnedRejections.find(key)) { |
| 120 | auto promise = getLocal(js.v8Isolate, item.promise); |
| 121 | if (!promise.IsEmpty()) { |
| 122 | AsyncContextFrame::Scope scope(js, tryGetFrame(item.asyncContextFrame)); |
| 123 | handler(js, v8::kPromiseHandlerAddedAfterReject, jsg::HashableV8Ref(js.v8Isolate, promise), |
| 124 | js.v8Ref(js.v8Undefined())); |
| 125 | } |
| 126 | warnedRejections.release(item); |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | void UnhandledRejectionHandler::ensureProcessingWarnings(jsg::Lock& js) { |
| 131 | if (scheduled) { |
| 132 | return; |
| 133 | } |
| 134 | scheduled = true; |
| 135 | if (useMicrotasksCompletedCallback) { |
| 136 | // Schedule processing to run after the microtask checkpoint completes. |
| 137 | // This ensures that promise chains like `.then().catch()` have fully settled |
| 138 | // before we decide a rejection is unhandled. Using a microtask would race |
| 139 | // with V8's internal promise adoption microtasks and fire too early. |
| 140 | // See https://github.com/cloudflare/workerd/issues/6020 |
| 141 | js.v8Isolate->AddMicrotasksCompletedCallback( |
| 142 | &UnhandledRejectionHandler::onMicrotasksCompleted, this); |
| 143 | // Ensure we get another microtask checkpoint to deliver the callback even if |
| 144 | // we're already past the current one. |
| 145 | js.requestExtraMicrotaskCheckpoint(); |
| 146 | } else { |
| 147 | js.resolvedPromise().then(js, [this](jsg::Lock& js) { processWarnings(js); }); |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | void UnhandledRejectionHandler::onMicrotasksCompleted(v8::Isolate* isolate, void* data) { |
| 152 | auto* handler = static_cast<UnhandledRejectionHandler*>(data); |
| 153 | KJ_DEFER(isolate->RemoveMicrotasksCompletedCallback( |
| 154 | &UnhandledRejectionHandler::onMicrotasksCompleted, data)); |
| 155 | auto& js = Lock::from(isolate); |
| 156 | KJ_TRY { |
| 157 | handler->processWarnings(js); |
| 158 | |
| 159 | // Ensure microtasks scheduled by unhandledrejection handlers run promptly. |
| 160 | js.requestExtraMicrotaskCheckpoint(); |
| 161 | } |
| 162 | KJ_CATCH(exception) { |
| 163 | handler->scheduled = false; |
| 164 | KJ_LOG(ERROR, "uncaught exception while processing unhandled rejections", exception); |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | void UnhandledRejectionHandler::processWarnings(jsg::Lock& js) { |
| 169 | scheduled = false; |
| 170 | warnedRejections.eraseAll([](auto& value) { return !value.isAlive(); }); |
| 171 | |
| 172 | while (unhandledRejections.size() > 0) { |
| 173 | auto entry = unhandledRejections.release(*unhandledRejections.begin()); |
| 174 | |
| 175 | if (!entry.isAlive()) { |
| 176 | continue; |
| 177 | } |
| 178 | |
| 179 | auto promise = getLocal(js.v8Isolate, entry.promise); |
| 180 | auto value = getLocal(js.v8Isolate, entry.value); |
| 181 | |
| 182 | AsyncContextFrame::Scope scope(js, tryGetFrame(entry.asyncContextFrame)); |
| 183 | |
| 184 | // Most of the time it shouldn't be found but there are times where it can |
| 185 | // be duplicated -- such as when a promise gets rejected multiple times. |
| 186 | // Check quickly before inserting to avoid a crash. |
| 187 | // Keep strong refs through dispatch, then downgrade to weak to avoid leaks. |
| 188 | entry.promise.SetWeak(); |
| 189 | entry.value.SetWeak(); |
| 190 | warnedRejections.upsert( |
| 191 | kj::mv(entry), [](UnhandledRejection& existing, UnhandledRejection&& replacement) { |
| 192 | // We're just going to ignore if the unhandled rejection was already here. |
| 193 | }); |
| 194 | |
| 195 | js.tryCatch([&] { |
| 196 | handler(js, v8::kPromiseRejectWithNoHandler, jsg::HashableV8Ref(js.v8Isolate, promise), |
| 197 | js.v8Ref(value)); |
| 198 | }, [&](Value exception) { |
| 199 | // If any exceptions occur while reporting the event, we will log them |
| 200 | // but otherwise ignore them. We do not want such errors to be fatal here. |
| 201 | if (js.areWarningsLogged()) { |
| 202 | js.logWarning( |
| 203 | kj::str("Exception while logging unhandled rejection:", exception.getHandle(js))); |
| 204 | } |
| 205 | }); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | void UnhandledRejectionHandler::UnhandledRejection::visitForMemoryInfo( |
| 210 | MemoryTracker& tracker) const { |
| 211 | tracker.trackField("asyncContextFrame", asyncContextFrame); |
| 212 | } |
| 213 | |
| 214 | } // namespace workerd::jsg |