Skip to content
File

Blob: src/workerd/jsg/util.c++

38.1 KB
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#include "jsg.h" // can't include util.h directly due to weird cyclic dependency...
7#include "ser.h"
8#include "setup.h"
9 
10#include <workerd/jsg/exception-metadata.capnp.h>
11#include <workerd/util/entropy.h>
12 
13#include <capnp/message.h>
14#include <capnp/serialize.h>
15#include <kj/debug.h>
16 
17#include <cstdlib>
18#include <set>
19 
20#if !_WIN32
21#include <cxxabi.h>
22#endif
23 
24namespace workerd::jsg {
25 
26bool getCaptureThrowsAsRejections(v8::Isolate* isolate) {
27 auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
28 return jsgIsolate.getCaptureThrowsAsRejections();
29}
30 
31bool getShouldSetToStringTag(v8::Isolate* isolate) {
32 auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
33 return jsgIsolate.shouldSetToStringTag();
34}
35 
36bool getShouldSetImmutablePrototype(v8::Isolate* isolate) {
37 auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
38 return jsgIsolate.shouldSetImmutablePrototype();
39}
40 
41bool getSpecCompliantPropertyAttributes(v8::Isolate* isolate) {
42 auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
43 return jsgIsolate.shouldUseSpecCompliantPropertyAttributes();
44}
45 
46#if _WIN32
47kj::String fullyQualifiedTypeName(const std::type_info& type) {
48 // type.name() returns a human-readable name on Windows:
49 // https://learn.microsoft.com/en-us/cpp/cpp/type-info-class?view=msvc-170
50 kj::StringPtr name = type.name();
51 
52 // Remove struct prefix
53 if (name.startsWith("struct ")) {
54 name = name.slice(7);
55 }
56 // Remove class prefix
57 if (name.startsWith("class ")) {
58 name = name.slice(6);
59 }
60 
61 kj::String result = kj::str(name);
62 
63 // Replace instances of `anonymous namespace' with (anonymous namespace)
64 for (auto& c: result.asArray()) {
65 if (c == '`')
66 c = '(';
67 else if (c == '\'')
68 c = ')';
69 }
70 
71 return kj::mv(result);
72}
73#else
74kj::String fullyQualifiedTypeName(const std::type_info& type) {
75 int status;
76 char* buf = abi::__cxa_demangle(type.name(), nullptr, nullptr, &status);
77 kj::String result = kj::str(buf == nullptr ? type.name() : buf);
78 free(buf);
79 
80 return kj::mv(result);
81}
82#endif
83 
84kj::String typeName(const std::type_info& type) {
85 auto result = fullyQualifiedTypeName(type);
86 
87 // Strip namespace, if any.
88 KJ_IF_SOME(pos, result.findLast(':')) {
89 result = kj::str(result.slice(pos + 1));
90 }
91 
92 // Strip template args, if any.
93 //
94 // TODO(someday): Maybe just strip namespaces from each arg?
95 KJ_IF_SOME(pos, result.findFirst('<')) {
96 result = kj::str(result.first(pos));
97 }
98 
99 return kj::mv(result);
100}
101 
102namespace {
103 
104// For internal errors, we generate an ID to include when rendering user-facing "internal error"
105// exceptions and writing internal exception logs, to make it easier to search for logs
106// corresponding to "internal error" exceptions reported by users.
107//
108// We'll use an ID of 24 base-32 encoded characters, just because its relatively simple to
109// generate from random bytes. This should give us a value with 120 bits of uniqueness, which is
110// about as good as a UUID.
111//
112// (We're not using base-64 encoding to avoid issues with case insensitive search, as well as
113// ensuring that the id is easy to select and copy via double-clicking.)
114using InternalErrorId = kj::FixedArray<char, 24>;
115 
116constexpr char BASE32_DIGITS[] = "0123456789abcdefghijklmnopqrstuv";
117 
118InternalErrorId makeInternalErrorId() {
119 InternalErrorId id;
120 if (isPredictableModeForTest()) {
121 // In testing mode, use content that generates a "0123456789abcdefghijklm" ID:
122 for (auto i: kj::indices(id)) {
123 id[i] = i;
124 }
125 } else {
126 getEntropy(kj::asBytes(id));
127 }
128 for (auto i: kj::indices(id)) {
129 id[i] = BASE32_DIGITS[static_cast<unsigned char>(id[i]) % 32];
130 }
131 return id;
132}
133 
134kj::String renderInternalError(InternalErrorId& internalErrorId) {
135 return kj::str("internal error; reference = ", internalErrorId);
136}
137 
138} // namespace
139 
140v8::Local<v8::Value> makeInternalError(v8::Isolate* isolate, kj::StringPtr internalMessage) {
141 auto wdErrId = makeInternalErrorId();
142 KJ_LOG(ERROR, internalMessage, wdErrId);
143 return v8::Exception::Error(v8Str(isolate, renderInternalError(wdErrId)));
144}
145 
146namespace {
147 
148kj::StringPtr trimErrorMessage(kj::StringPtr errorString) {
149 // For strings beginning with ':' OWS, returns everything after the OWS. Otherwise returns the
150 // empty string.
151 if (errorString.startsWith(":")) {
152 errorString = errorString.slice(1);
153 while (errorString.startsWith(" ")) {
154 errorString = errorString.slice(1);
155 }
156 return errorString;
157 }
158 return "";
159}
160 
161bool setRemoteError(v8::Isolate* isolate, v8::Local<v8::Value>& exception) {
162 // If an exception was tunneled, we add a property `.remote` to the Javascript error.
163 KJ_ASSERT(exception->IsObject());
164 auto obj = exception.As<v8::Object>();
165 return jsg::check(obj->Set(
166 isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "remote"_kj), v8::True(isolate)));
167}
168 
169bool setRetryableError(v8::Isolate* isolate, v8::Local<v8::Value>& exception) {
170 KJ_ASSERT(exception->IsObject());
171 auto obj = exception.As<v8::Object>();
172 return jsg::check(obj->Set(
173 isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "retryable"_kj), v8::True(isolate)));
174}
175 
176bool setOverloadedError(v8::Isolate* isolate, v8::Local<v8::Value>& exception) {
177 KJ_ASSERT(exception->IsObject());
178 auto obj = exception.As<v8::Object>();
179 return jsg::check(obj->Set(
180 isolate->GetCurrentContext(), jsg::v8StrIntern(isolate, "overloaded"_kj), v8::True(isolate)));
181}
182 
183bool setDurableObjectResetError(v8::Isolate* isolate, v8::Local<v8::Value>& exception) {
184 KJ_ASSERT(exception->IsObject());
185 auto obj = exception.As<v8::Object>();
186 return jsg::check(obj->Set(isolate->GetCurrentContext(),
187 jsg::v8StrIntern(isolate, "durableObjectReset"_kj), v8::True(isolate)));
188}
189struct DecodedException {
190 v8::Local<v8::Value> handle;
191 bool isInternal;
192 bool isFromRemote;
193 bool isDurableObjectReset;
194 // TODO(cleanup): Maybe<> is redundant with isInternal flag field?
195 kj::Maybe<InternalErrorId> internalErrorId;
196 bool isDisconnection;
197 bool isDoNotLogException;
198};
199 
200DecodedException decodeTunneledException(
201 v8::Isolate* isolate, const kj::Exception& exception, const ExceptionToJsOptions& options) {
202 
203 // We currently support tunneling the following error types:
204 //
205 // - Error: While the Web IDL spec claims this is reserved for use by program authors, this
206 // is broadly useful as a general-purpose error type.
207 // - RangeError: Commonly thrown by web API implementations.
208 // - TypeError: Commonly thrown by web API implementations.
209 // - SyntaxError: Especially from JSON parsing.
210 // - ReferenceError: Not thrown by our APIs, but could be tunneled from user code.
211 // - DOMException: Commonly thrown by web API implementations.
212 //
213 // https://heycam.github.io/webidl/#idl-exceptions
214 //
215 // TODO(someday): Support arbitrary user-defined error types, not just Error?
216 auto tunneledInfo = tunneledErrorType(exception.getDescription());
217 DecodedException result;
218 result.isDisconnection = false;
219 result.isDoNotLogException = tunneledInfo.isDoNotLogException;
220 
221 auto errorType = tunneledInfo.message;
222 auto appMessage = [&](kj::StringPtr errorString) -> kj::String {
223 if (tunneledInfo.isInternal) {
224 result.internalErrorId = makeInternalErrorId();
225 return renderInternalError(KJ_ASSERT_NONNULL(result.internalErrorId));
226 } else {
227 return kj::str(trimErrorMessage(errorString));
228 }
229 };
230 result.isInternal = tunneledInfo.isInternal;
231 result.isFromRemote = tunneledInfo.isFromRemote;
232 result.isDurableObjectReset = tunneledInfo.isDurableObjectReset;
233 
234 auto addAdditionalInfo = [isolate, &result, &exception]() {
235 if (!result.handle->IsObject()) return;
236 // Note that if the error was deserialized from the TUNNELED_EXCEPTION_DETAIL_ID detail,
237 // these operations may overwrite properties that were already set on the serialized
238 // error object. That is fine, we want the metadata captured in the kj::Exception
239 // description to take precedence.
240 // TODO(someday): Maybe consider making this configurable when a deserialized
241 // error is used?
242 if (result.isFromRemote) {
243 setRemoteError(isolate, result.handle);
244 }
245 
246 if (exception.getType() == kj::Exception::Type::DISCONNECTED) {
247 setRetryableError(isolate, result.handle);
248 } else if (exception.getType() == kj::Exception::Type::OVERLOADED) {
249 setOverloadedError(isolate, result.handle);
250 }
251 
252 if (result.isDurableObjectReset) {
253 setDurableObjectResetError(isolate, result.handle);
254 }
255 };
256 
257 if (tunneledInfo.isJsgError) {
258 // DOMExceptions require a parenthesized error name argument, like DOMException(SyntaxError).
259 // TODO(someday): We always handle DOMException specifically here rather than decoding from
260 // the serialized detail because using the detail breaks some tests that expect a specific
261 // error details. We'll need to investigate further to see if we can make this more consistent.
262 if (errorType.startsWith("DOMException(")) {
263 errorType = errorType.slice(strlen("DOMException("));
264 // Check for closing brace
265 KJ_IF_SOME(closeParen, errorType.findFirst(')')) {
266 auto& js = Lock::from(isolate);
267 auto errorName = kj::str(errorType.first(closeParen));
268 auto message = appMessage(errorType.slice(1 + closeParen));
269 auto exception = js.domException(kj::mv(errorName), kj::mv(message));
270 result.handle = KJ_ASSERT_NONNULL(exception.tryGetHandle(js));
271 addAdditionalInfo();
272 return result;
273 }
274 }
275 
276 auto& isolateBase = IsolateBase::from(isolate);
277 if ((options.trusted || isolateBase.getUsingEnhancedErrorSerialization()) &&
278 !options.ignoreDetail) {
279 // If the error was originally converted from a JS error, then we likely have
280 // serialized the original error object as a detail, if so, let's try to use
281 // that, otherwise, we'll fall back to constructing a new error object. If
282 // the ignoreDetail optiom is set, we skip trying to deserialize.
283 KJ_IF_SOME(serializedJsError, exception.getDetail(jsg::TUNNELED_EXCEPTION_DETAIL_ID)) {
284 kj::Maybe<jsg::JsValue> deserialized;
285 v8::TryCatch tryCatch(isolate);
286 try {
287 auto& js = Lock::from(isolate);
288 jsg::Deserializer deser(js, serializedJsError, kj::none, kj::none,
289 jsg::Deserializer::Options{
290 // By default, we do not preserve stacks in deserialized errors because
291 // of concerns sharing stack details over potentially untrusted boundaries.
292 // However, if the caller has explicitly indicated that the scope it trusted,
293 // we will preserve the stack in the deserialized error.
294 .preserveStackInErrors = options.trusted,
295 });
296 result.handle = deser.readValue(js);
297 
298 // If the result came from a serialized JS detail, it might not be an object!
299 // If that's the case, and allowNonObjects is false (the default), we will ignore
300 // the deserialized data and fallback to the normal decoding below.
301 if (result.handle->IsObject() || options.allowNonObjects) {
302 addAdditionalInfo();
303 return result;
304 }
305 } catch (jsg::JsExceptionThrown&) {
306 if (!tryCatch.CanContinue()) {
307 tryCatch.ReThrow();
308 throw;
309 }
310 // Failed to deserialize, we'll ignore the error and continue with the original
311 // decoding below. For debugging purposes, when verbose logging is enabled, we
312 // will at least log the error.
313 if (!tryCatch.Exception().IsEmpty()) {
314 KJ_LOG(INFO, "Failed to deserialize tunneled JS error detail", tryCatch.Exception());
315 } else {
316 KJ_LOG(INFO, "Failed to deserialize tunneled JS error detail (unknown error)");
317 }
318 }
319 }
320 }
321 
322 // It's neither a DOMException nor are we using a serialized detail, so it must be
323 // one of the standard JS error types (or we will treat it as such).
324#define HANDLE_AND_RETURN_V8_ERROR(error_name, error_type) \
325 if (errorType.startsWith(error_name)) { \
326 auto message = appMessage(errorType.slice(strlen(error_name))); \
327 result.handle = v8::Exception::error_type(v8Str(isolate, message)); \
328 addAdditionalInfo(); \
329 return result; \
330 }
331 
332 JS_ERROR_TYPES(HANDLE_AND_RETURN_V8_ERROR)
333#undef HANDLE_AND_RETURN_V8_ERROR
334 }
335 
336 // It's not a tunneled JavaScript error type that we recognize.
337 // Return an internal error.
338 result.isInternal = true;
339 
340 // For disconnection errors, we ignore any tunneled error info and just return
341 // a generic "Network connection lost" error. One thing to keep in mind is that
342 // DOMExceptions with the AbortError name are also DISCONNECTED errors, but those
343 // are handled above as tunneled JS errors. It is important that we preserve the
344 // ordering of these checks, so keep this if block after the tunneled JS error
345 // handling above.
346 if (exception.getType() == kj::Exception::Type::DISCONNECTED) {
347 result.isDisconnection = true;
348 result.handle = v8::Exception::Error(v8StrIntern(isolate, "Network connection lost."_kj));
349 if (tunneledInfo.isFromRemote) {
350 setRemoteError(isolate, result.handle);
351 }
352 
353 // DISCONNECTED exceptions are considered retryable
354 setRetryableError(isolate, result.handle);
355 
356 if (tunneledInfo.isDurableObjectReset) {
357 setDurableObjectResetError(isolate, result.handle);
358 }
359 } else {
360 // For everything return a generic error with an internal error id.
361 result.internalErrorId = makeInternalErrorId();
362 result.handle = v8::Exception::Error(
363 v8Str(isolate, renderInternalError(KJ_ASSERT_NONNULL(result.internalErrorId))));
364 addAdditionalInfo();
365 }
366 return result;
367}
368 
369} // namespace
370 
371kj::StringPtr extractTunneledExceptionDescription(kj::StringPtr message) {
372 auto tunneledError = tunneledErrorType(message);
373 if (tunneledError.isInternal) {
374 // TODO(soon): Include an internal error ID in message, and also return the id.
375 return "Error: internal error";
376 } else {
377 return tunneledError.message;
378 }
379}
380 
381v8::Local<v8::Value> exceptionToJs(
382 v8::Isolate* isolate, kj::Exception&& exception, ExceptionToJsOptions options) {
383 // TODO(cleanup): decodeTunneledException is currently only used here, consider
384 // inlining it back into this function.
385 auto tunneledException = decodeTunneledException(isolate, exception, options);
386 
387 if (tunneledException.isInternal) {
388 // Don't log exceptions that have been explicitly marked with worker_do_not_log or are
389 // DISCONNECTED exceptions as these are unlikely to represent bugs worth tracking.
390 bool shouldLogWithInternalId =
391 !tunneledException.isDisconnection && !tunneledException.isDoNotLogException;
392 auto& observer = IsolateBase::from(isolate).getObserver();
393 observer.reportInternalException(exception,
394 {
395 .isInternal = tunneledException.isInternal,
396 .isFromRemote = tunneledException.isFromRemote,
397 .isDurableObjectReset = tunneledException.isDurableObjectReset,
398 .internalErrorId = shouldLogWithInternalId ? tunneledException.internalErrorId : kj::none,
399 });
400 if (shouldLogWithInternalId) {
401 // LOG_EXCEPTION("jsgInternalError", ...), but with internal error ID:
402 auto& e = exception;
403 constexpr auto sentryErrorContext = "jsgInternalError";
404 auto& wdErrId = KJ_ASSERT_NONNULL(tunneledException.internalErrorId);
405 KJ_LOG(ERROR, e, sentryErrorContext, wdErrId);
406 } else {
407 KJ_LOG(INFO, exception); // Run with --verbose to see exception logs.
408 }
409 }
410 
411 return tunneledException.handle;
412}
413 
414Value Lock::exceptionToJs(kj::Exception&& exception, ExceptionToJsOptions options) {
415 return withinHandleScope(
416 [&] { return Value(v8Isolate, jsg::exceptionToJs(v8Isolate, kj::mv(exception), options)); });
417}
418 
419JsRef<JsValue> Lock::exceptionToJsValue(kj::Exception&& exception, ExceptionToJsOptions options) {
420 return withinHandleScope([&] {
421 JsValue val = JsValue(jsg::exceptionToJs(v8Isolate, kj::mv(exception), options));
422 return val.addRef(*this);
423 });
424}
425 
426void Lock::throwException(Value&& exception) {
427 withinHandleScope([&] { v8Isolate->ThrowException(exception.getHandle(*this)); });
428 throw JsExceptionThrown();
429}
430 
431void Lock::throwException(const JsValue& exception) {
432 withinHandleScope([&] { v8Isolate->ThrowException(exception); });
433 throw JsExceptionThrown();
434}
435 
436void throwInternalError(v8::Isolate* isolate, kj::StringPtr internalMessage) {
437 isolate->ThrowException(makeInternalError(isolate, internalMessage));
438}
439 
440void throwInternalError(
441 v8::Isolate* isolate, kj::Exception&& exception, ExceptionToJsOptions options) {
442 KJ_IF_SOME(renderingError, kj::runCatchingExceptions([&]() {
443 isolate->ThrowException(exceptionToJs(isolate, kj::mv(exception), options));
444 })) {
445 KJ_LOG(ERROR, "error rendering exception", renderingError);
446 KJ_LOG(ERROR, exception);
447 throwInternalError(isolate, "error rendering exception");
448 }
449}
450 
451void addExceptionDetail(Lock& js, kj::Exception& exception, v8::Local<v8::Value> handle) {
452 v8::TryCatch tryCatch(js.v8Isolate);
453 try {
454 Serializer ser(js,
455 {// Make sure we don't break compatibility if V8 introduces a new version. This value can
456 // be bumped to match the new version once all of production is updated to understand it.
457 .version = 15});
458 ser.write(js, JsValue(handle));
459 exception.setDetail(TUNNELED_EXCEPTION_DETAIL_ID, ser.release().data);
460 } catch (JsExceptionThrown&) {
461 // Either:
462 // a. The exception is not serializable, and we caught the exception. We will just ignore it
463 // and proceed without annotating.
464 // b. The isolate's execution is being terminated, and so tryCatch.CanContinue() is false. In
465 // this case we cannot serialize the exception, but again we'll just move on without the
466 // annotation.
467 }
468}
469 
470void addJsExceptionMetadata(Lock& js, kj::Exception& exception, v8::Local<v8::Value> handle) {
471 // Extract JavaScript error type and stack trace
472 if (!handle->IsObject()) {
473 return; // Not an error object, nothing to extract
474 }
475 
476 auto errorObj = jsg::JsObject(handle.As<v8::Object>());
477 
478 // Build Cap'n Proto message
479 capnp::MallocMessageBuilder message;
480 auto metadata = message.initRoot<JsExceptionMetadata>();
481 
482 // Limit for user-controlled fields (4KB)
483 constexpr size_t MAX_FIELD_SIZE = 4096;
484 
485 // Extract error name (e.g., "Error", "TypeError", "RangeError")
486 auto nameProp = errorObj.get(js, "name"_kj);
487 if (nameProp.isString()) {
488 auto errorType = nameProp.toString(js);
489 // Truncate to 4KB if needed
490 if (errorType.size() > MAX_FIELD_SIZE) {
491 errorType = kj::str(errorType.slice(0, MAX_FIELD_SIZE));
492 }
493 metadata.setErrorType(errorType);
494 }
495 
496 // Extract stack trace string
497 auto stackProp = errorObj.get(js, "stack"_kj);
498 if (stackProp.isString()) {
499 auto stackTrace = stackProp.toString(js);
500 // Truncate to 4KB if needed
501 if (stackTrace.size() > MAX_FIELD_SIZE) {
502 stackTrace = kj::str(stackTrace.slice(0, MAX_FIELD_SIZE));
503 }
504 metadata.setStackTrace(stackTrace);
505 }
506 
507 // Serialize to bytes using Cap'n Proto
508 auto words = capnp::messageToFlatArray(message);
509 exception.setDetail(JS_EXCEPTION_METADATA_DETAIL_ID, kj::heapArray(words.asBytes()));
510}
511 
512static kj::String typeErrorMessage(TypeErrorContext c, const char* expectedType) {
513 kj::String type;
514 
515 KJ_IF_SOME(t, c.type) {
516 type = typeName(t);
517 }
518 
519 switch (c.kind) {
520 case TypeErrorContext::METHOD_ARGUMENT:
521 return kj::str("Failed to execute '", c.memberName, "' on '", type, "': parameter ",
522 c.argumentIndex + 1, " is not of type '", expectedType, "'.");
523 case TypeErrorContext::CONSTRUCTOR_ARGUMENT:
524 return kj::str("Failed to construct '", type, "': constructor parameter ",
525 c.argumentIndex + 1, " is not of type '", expectedType, "'.");
526 case TypeErrorContext::SETTER_ARGUMENT:
527 return kj::str("Failed to set the '", c.memberName, "' property on '", type,
528 "': the provided value is not of type '", expectedType, "'.");
529 case TypeErrorContext::STRUCT_FIELD:
530 return kj::str("Incorrect type for the '", c.memberName, "' field on '", type,
531 "': the provided value is not of type '", expectedType, "'.");
532 case TypeErrorContext::ARRAY_ELEMENT:
533 return kj::str("Incorrect type for array element ", c.argumentIndex,
534 ": the provided value is not of type '", expectedType, "'.");
535 case TypeErrorContext::CALLBACK_ARGUMENT:
536 return kj::str("Failed to execute function: parameter ", c.argumentIndex + 1,
537 " is not of type '", expectedType, "'.");
538 case TypeErrorContext::CALLBACK_RETURN:
539 return kj::str("Callback returned incorrect type; expected '", expectedType, "'");
540 case TypeErrorContext::DICT_KEY:
541 return kj::str("Incorrect type for map entry '", c.memberName,
542 "': the provided key is not of type '", expectedType, "'.");
543 case TypeErrorContext::DICT_FIELD:
544 return kj::str("Incorrect type for map entry '", c.memberName,
545 "': the provided value is not of type '", expectedType, "'.");
546 case TypeErrorContext::PROMISE_RESOLUTION:
547 return kj::str(
548 "Incorrect type for Promise: the Promise did not resolve to '", expectedType, "'.");
549 case TypeErrorContext::OTHER:
550 return kj::str("Incorrect type: the provided value is not of type '", expectedType, "'.");
551 };
552 
553 KJ_UNREACHABLE;
554}
555 
556static kj::String unimplementedErrorMessage(TypeErrorContext c) {
557 kj::String type;
558 
559 KJ_IF_SOME(t, c.type) {
560 type = typeName(t);
561 }
562 
563 switch (c.kind) {
564 case TypeErrorContext::METHOD_ARGUMENT:
565 return kj::str("Failed to execute '", c.memberName, "' on '", type, "': parameter ",
566 c.argumentIndex + 1, " is not implemented.");
567 case TypeErrorContext::CONSTRUCTOR_ARGUMENT:
568 return kj::str("Failed to construct '", type, "': constructor parameter ",
569 c.argumentIndex + 1, " is not implemented.");
570 case TypeErrorContext::SETTER_ARGUMENT:
571 return kj::str("Failed to set the '", c.memberName, "' property on '", type,
572 "': the ability to set this property is not implemented.");
573 case TypeErrorContext::STRUCT_FIELD:
574 return kj::str("The '", c.memberName, "' field on '", type, "' is not implemented.");
575 case TypeErrorContext::ARRAY_ELEMENT:
576 KJ_UNREACHABLE;
577 case TypeErrorContext::CALLBACK_ARGUMENT:
578 return kj::str(
579 "Failed to execute function: parameter ", c.argumentIndex + 1, " is not implemented.");
580 case TypeErrorContext::CALLBACK_RETURN:
581 KJ_UNREACHABLE;
582 case TypeErrorContext::DICT_KEY:
583 KJ_UNREACHABLE;
584 case TypeErrorContext::DICT_FIELD:
585 KJ_UNREACHABLE;
586 case TypeErrorContext::PROMISE_RESOLUTION:
587 KJ_UNREACHABLE;
588 case TypeErrorContext::OTHER:
589 KJ_UNREACHABLE;
590 };
591 
592 KJ_UNREACHABLE;
593}
594 
595void throwTypeError(v8::Isolate* isolate, kj::StringPtr message) {
596 isolate->ThrowException(v8::Exception::TypeError(v8Str(isolate, message)));
597 throw JsExceptionThrown();
598}
599 
600void throwTypeError(v8::Isolate* isolate, TypeErrorContext errorContext, kj::String expectedType) {
601 kj::String message = typeErrorMessage(errorContext, expectedType.cStr());
602 throwTypeError(isolate, message);
603}
604 
605void throwTypeError(v8::Isolate* isolate, TypeErrorContext errorContext, const char* expectedType) {
606 kj::String message = typeErrorMessage(errorContext, expectedType);
607 throwTypeError(isolate, message);
608}
609 
610void throwTypeError(
611 v8::Isolate* isolate, TypeErrorContext errorContext, const std::type_info& expectedType) {
612 if (expectedType == typeid(Unimplemented)) {
613 isolate->ThrowError(v8StrIntern(isolate, unimplementedErrorMessage(errorContext)));
614 throw JsExceptionThrown();
615 } else {
616 throwTypeError(isolate, errorContext, typeName(expectedType).cStr());
617 }
618}
619 
620static constexpr auto kIllegalConstructorMessage = "Illegal constructor";
621 
622void throwIllegalConstructor(const v8::FunctionCallbackInfo<v8::Value>& args) {
623 auto isolate = args.GetIsolate();
624 isolate->ThrowException(
625 v8::Exception::TypeError(v8StrIntern(isolate, kIllegalConstructorMessage)));
626}
627 
628void throwTunneledException(v8::Isolate* isolate, v8::Local<v8::Value> exception) {
629 kj::throwFatalException(createTunneledException(isolate, exception));
630}
631 
632kj::Exception createTunneledException(v8::Isolate* isolate, v8::Local<v8::Value> exception) {
633 auto& jsgIsolate = *reinterpret_cast<IsolateBase*>(isolate->GetData(SET_DATA_ISOLATE_BASE));
634 auto& js = Lock::from(isolate);
635 return jsgIsolate.unwrapException(js, isolate->GetCurrentContext(), exception);
636}
637 
638kj::Exception Lock::exceptionToKj(Value&& exception) {
639 return withinHandleScope(
640 [&] { return createTunneledException(v8Isolate, exception.getHandle(*this)); });
641}
642 
643kj::Exception Lock::exceptionToKj(const JsValue& exception) {
644 return withinHandleScope([&] { return createTunneledException(v8Isolate, exception); });
645}
646 
647static kj::byte DUMMY = 0;
648static kj::Array<kj::byte> getEmptyArray() {
649 // An older version of asBytes(), when given an empty ArrayBuffer, would often return an array
650 // with zero size but non-empty start address. Meanwhile, it turns out that some code,
651 // particularly in BoringSSL, does not like receiving a null pointer even when the length is
652 // zero -- it will spuriously produce an error. We could carefully find all the places where
653 // this is an issue and adjust the specific calls to avoid passing null pointers, but it is
654 // easier to change `asBytes()` so that it never produces a null start address in the first
655 // place.
656 return kj::Array<kj::byte>(&DUMMY, 0, kj::NullArrayDisposer::instance);
657}
658 
659kj::Array<kj::byte> asBytes(v8::Local<v8::ArrayBuffer> arrayBuffer) {
660 auto backing = arrayBuffer->GetBackingStore();
661 kj::ArrayPtr bytes(static_cast<kj::byte*>(backing->Data()), backing->ByteLength());
662 if (bytes == nullptr) {
663 return getEmptyArray();
664 } else {
665 return bytes.attach(kj::mv(backing));
666 }
667}
668kj::Array<kj::byte> asBytes(v8::Local<v8::ArrayBufferView> arrayBufferView) {
669 auto backing = arrayBufferView->Buffer()->GetBackingStore();
670 kj::ArrayPtr buffer(static_cast<kj::byte*>(backing->Data()), backing->ByteLength());
671 auto sliceStart = arrayBufferView->ByteOffset();
672 auto sliceEnd = sliceStart + arrayBufferView->ByteLength();
673 KJ_ASSERT(buffer.size() >= sliceEnd);
674 auto bytes = buffer.slice(sliceStart, sliceEnd);
675 if (bytes == nullptr) {
676 return getEmptyArray();
677 } else {
678 return bytes.attach(kj::mv(backing));
679 }
680}
681 
682// TODO(soon): If the returned kj::Array<kj::byte> is used outside of the isolate lock,
683// we'll need to ensure it works correctly once MPK (Memory Protection Keys) enforcement
684// is fully in place.
685kj::Array<kj::byte> asBytes(v8::Local<v8::SharedArrayBuffer> sharedArrayBuffer) {
686 auto backing = sharedArrayBuffer->GetBackingStore();
687 kj::ArrayPtr bytes(static_cast<kj::byte*>(backing->Data()), backing->ByteLength());
688 if (bytes == nullptr) {
689 return getEmptyArray();
690 } else {
691 return bytes.attach(kj::mv(backing));
692 }
693}
694 
695void recursivelyFreeze(v8::Local<v8::Context> context, v8::Local<v8::Value> value) {
696 if (value->IsArray()) {
697 // Optimize array freezing (Array is a subclass of Object, but we can iterate it faster).
698 v8::HandleScope scope(v8::Isolate::GetCurrent());
699 auto arr = value.As<v8::Array>();
700 
701 for (auto i: kj::zeroTo(arr->Length())) {
702 recursivelyFreeze(context, check(arr->Get(context, i)));
703 }
704 
705 check(arr->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen));
706 } else if (value->IsObject()) {
707 v8::HandleScope scope(v8::Isolate::GetCurrent());
708 auto obj = value.As<v8::Object>();
709 auto names = check(obj->GetPropertyNames(context, v8::KeyCollectionMode::kOwnOnly,
710 v8::ALL_PROPERTIES, v8::IndexFilter::kIncludeIndices));
711 
712 for (auto i: kj::zeroTo(names->Length())) {
713 recursivelyFreeze(context, check(obj->Get(context, check(names->Get(context, i)))));
714 }
715 
716 check(obj->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen));
717 } else {
718 // Primitive type, nothing to do.
719 }
720}
721 
722v8::Local<v8::Value> deepClone(v8::Local<v8::Context> context, v8::Local<v8::Value> value) {
723 // This is implemented in the classic JSON restringification way.
724 auto serialized = check(v8::JSON::Stringify(context, value));
725 return check(v8::JSON::Parse(context, serialized));
726}
727 
728namespace {
729v8::MaybeLocal<v8::Value> makeRejectedPromise(
730 v8::Isolate* isolate, v8::Local<v8::Value> exception) {
731 v8::Local<v8::Promise::Resolver> resolver;
732 auto context = isolate->GetCurrentContext();
733 if (!v8::Promise::Resolver::New(context).ToLocal(&resolver) ||
734 resolver->Reject(context, exception).IsNothing()) {
735 return v8::MaybeLocal<v8::Value>();
736 }
737 
738 return resolver->GetPromise();
739};
740 
741void returnRejectedPromiseImpl(auto info, v8::Local<v8::Value> exception, v8::TryCatch& tryCatch) {
742 v8::Local<v8::Value> promise;
743 if (!makeRejectedPromise(info.GetIsolate(), exception).ToLocal(&promise)) {
744 // If makeRejectedPromise fails, the tryCatch should have caught the error.
745 // Let's rethrow it if it isn't terminal.
746 if (tryCatch.CanContinue()) tryCatch.ReThrow();
747 }
748 info.GetReturnValue().Set(promise);
749}
750} // namespace
751 
752void returnRejectedPromise(const v8::FunctionCallbackInfo<v8::Value>& info,
753 v8::Local<v8::Value> exception,
754 v8::TryCatch& tryCatch) {
755 returnRejectedPromiseImpl<const v8::FunctionCallbackInfo<v8::Value>&>(info, exception, tryCatch);
756}
757 
758void returnRejectedPromise(const v8::PropertyCallbackInfo<v8::Value>& info,
759 v8::Local<v8::Value> exception,
760 v8::TryCatch& tryCatch) {
761 returnRejectedPromiseImpl<const v8::PropertyCallbackInfo<v8::Value>&>(info, exception, tryCatch);
762}
763 
764static ExternalStringAllocator& getAllocatorForIsolate(v8::Isolate* isolate) {
765 return IsolateBase::from(isolate).getExternalStringAllocator();
766}
767 
768// Default allocator that uses standard new/delete.
769// We typically don't use the new/delete operators directly,
770// but in this case we have to because V8's ExternalStringResource may default to `delete this`
771// if not overridden, and we are allocating raw byte arrays for placement new.
772class DefaultExternalStringAllocator final: public ExternalStringAllocator {
773 public:
774 void* allocate(size_t size) override {
775 return operator new(size);
776 }
777 void deallocate(void* ptr) override {
778 operator delete(ptr);
779 }
780};
781 
782kj::Own<ExternalStringAllocator> defaultExternalStringAllocator() {
783 static DefaultExternalStringAllocator allocator;
784 return kj::Own<ExternalStringAllocator>(&allocator, kj::NullDisposer::instance);
785}
786 
787// ======================================================================================
788 
789template <typename Type, typename Data>
790class ExternString: public Type {
791 // The implementation of ExternString here is very closely after the implementation of the same
792 // class in Node.js, with modifications to fit our conventions. It is distributed under the
793 // same MIT license that Node.js uses. The appropriate copyright attribution is included here:
794 //
795 // Copyright Node.js contributors. All rights reserved.
796 
797 // Permission is hereby granted, free of charge, to any person obtaining a copy
798 // of this software and associated documentation files (the "Software"), to
799 // deal in the Software without restriction, including without limitation the
800 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
801 // sell copies of the Software, and to permit persons to whom the Software is
802 // furnished to do so, subject to the following conditions:
803 
804 // The above copyright notice and this permission notice shall be included in
805 // all copies or substantial portions of the Software.
806 
807 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
808 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
809 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
810 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
811 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
812 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
813 // IN THE SOFTWARE.
814 
815 public:
816 inline const Data* data() const override {
817 return buf.begin();
818 }
819 
820 inline size_t length() const override {
821 return buf.size();
822 }
823 
824 inline uint64_t byteLength() const {
825 return length() * sizeof(Data);
826 }
827 
828 // Override Dispose() so that V8 properly deallocates through the configured
829 // ExternalStringAllocator rather than using `delete this` (the default).
830 void Dispose() override {
831 auto& allocator = getAllocatorForIsolate(isolate);
832 this->~ExternString();
833 allocator.deallocate(this);
834 }
835 
836 static v8::MaybeLocal<v8::String> createExtern(
837 v8::Isolate* isolate, kj::ArrayPtr<const Data>& buf) {
838 if (buf.size() == 0) {
839 return v8::String::Empty(isolate);
840 }
841 
842 // TODO(now): In Node.js impl, we check to see if length is less than a specified
843 // minimum. If it is, it's likely more efficient to just copy and use a regular
844 // heap allocated string than an external. We're not doing that here currently, but
845 // we might?
846 
847 auto& allocator = getAllocatorForIsolate(isolate);
848 auto mem = allocator.allocate(sizeof(ExternString<Type, Data>));
849 if (mem == nullptr) {
850 isolate->ThrowException(v8::Exception::Error(
851 v8::String::NewFromUtf8Literal(isolate, "String allocation failed")));
852 return v8::MaybeLocal<v8::String>();
853 }
854 
855 auto resource = new (mem) ExternString<Type, Data>(isolate, buf);
856 
857 v8::MaybeLocal<v8::String> str;
858 if constexpr (kj::isSameType<Type, v8::String::ExternalOneByteStringResource>()) {
859 str = v8::String::NewExternalOneByte(isolate, resource);
860 } else {
861 // resource here must be a v8::String::ExternalStringResource.
862 str = v8::String::NewExternalTwoByte(isolate, resource);
863 }
864 if (str.IsEmpty()) {
865 // This should happen only if the string is too long
866 resource->~ExternString<Type, Data>();
867 allocator.deallocate(mem);
868 isolate->ThrowException(v8::Exception::Error(
869 v8::String::NewFromUtf8Literal(isolate, "String allocation failed")));
870 return v8::MaybeLocal<v8::String>();
871 }
872 
873 return str;
874 }
875 
876 private:
877 v8::Isolate* isolate;
878 kj::ArrayPtr<const Data> buf;
879 
880 inline ExternString(v8::Isolate* isolate, kj::ArrayPtr<const Data>& buf)
881 : isolate(isolate),
882 buf(buf) {}
883};
884 
885using ExternOneByteString = ExternString<v8::String::ExternalOneByteStringResource, char>;
886using ExternTwoByteString = ExternString<v8::String::ExternalStringResource, uint16_t>;
887 
888v8::Local<v8::String> newExternalOneByteString(Lock& js, kj::ArrayPtr<const char> buf) {
889 return check(ExternOneByteString::createExtern(js.v8Isolate, buf));
890}
891 
892v8::Local<v8::String> newExternalTwoByteString(Lock& js, kj::ArrayPtr<const uint16_t> buf) {
893 return check(ExternTwoByteString::createExtern(js.v8Isolate, buf));
894}
895 
896// ======================================================================================
897// Module utilities
898 
899JsObject createMutableModuleExports(Lock& js, JsObject moduleNamespace) {
900 auto result = js.objNoProto();
901 auto names = moduleNamespace.getPropertyNames(js, OWN_ONLY, ALL_PROPERTIES, INCLUDE_INDICES);
902 
903 for (uint32_t i = 0; i < names.size(); i++) {
904 auto name = names.get(js, i);
905 result.set(js, name, moduleNamespace.get(js, name));
906 }
907 
908 return result;
909}
910 
911// ======================================================================================
912// Node.js Compat
913 
914namespace {
915// This list must be kept in sync with the list of builtins from Node.js.
916// It should be unlikely that anything is ever removed from this list, and
917// adding items to it is considered a semver-major change in Node.js.
918static const std::set<kj::StringPtr> NODEJS_BUILTINS{"_http_agent"_kj, "_http_client"_kj,
919 "_http_common"_kj, "_http_incoming"_kj, "_http_outgoing"_kj, "_http_server"_kj,
920 "_stream_duplex"_kj, "_stream_passthrough"_kj, "_stream_readable"_kj, "_stream_transform"_kj,
921 "_stream_wrap"_kj, "_stream_writable"_kj, "_tls_common"_kj, "_tls_wrap"_kj, "assert"_kj,
922 "assert/strict"_kj, "async_hooks"_kj, "buffer"_kj, "child_process"_kj, "cluster"_kj, "console"_kj,
923 "constants"_kj, "crypto"_kj, "dgram"_kj, "diagnostics_channel"_kj, "dns"_kj, "dns/promises"_kj,
924 "domain"_kj, "events"_kj, "fs"_kj, "fs/promises"_kj, "http"_kj, "http2"_kj, "https"_kj,
925 "inspector"_kj, "inspector/promises"_kj, "module"_kj, "net"_kj, "os"_kj, "path"_kj,
926 "path/posix"_kj, "path/win32"_kj, "perf_hooks"_kj, "process"_kj, "punycode"_kj, "querystring"_kj,
927 "readline"_kj, "readline/promises"_kj, "repl"_kj, "sqlite"_kj, "stream"_kj, "stream/consumers"_kj,
928 "stream/promises"_kj, "stream/web"_kj, "string_decoder"_kj, "sys"_kj, "timers"_kj,
929 "timers/promises"_kj, "tls"_kj, "trace_events"_kj, "tty"_kj, "url"_kj, "util"_kj, "util/types"_kj,
930 "v8"_kj, "vm"_kj, "wasi"_kj, "worker_threads"_kj, "zlib"_kj};
931} // namespace
932 
933kj::Maybe<kj::String> checkNodeSpecifier(kj::StringPtr specifier) {
934 // The sys module was renamed to 'util'. This shim remains to keep old programs
935 // working. `sys` is deprecated and shouldn't be used.
936 // Note to maintainers: Although this module has been deprecated for a while
937 // Node.js do not plan to remove it.
938 // See: https://github.com/nodejs/node/pull/35407#issuecomment-700693439
939 if (specifier == "sys" || specifier == "node:sys") [[unlikely]] {
940 return kj::str("node:util");
941 }
942 if (NODEJS_BUILTINS.contains(specifier)) {
943 return kj::str("node:", specifier);
944 } else if (specifier.startsWith("node:")) {
945 return kj::str(specifier);
946 }
947 return kj::none;
948}
949 
950bool isNodeJsCompatEnabled(jsg::Lock& js) {
951 return IsolateBase::from(js.v8Isolate).isNodeJsCompatEnabled();
952}
953 
954bool isNodeJsProcessV2Enabled(jsg::Lock& js) {
955 return IsolateBase::from(js.v8Isolate).isNodeJsProcessV2Enabled();
956}
957 
958bool isRequireReturnsDefaultExportEnabled(jsg::Lock& js) {
959 return IsolateBase::from(js.v8Isolate).isRequireReturnsDefaultExportEnabled();
960}
961 
962} // namespace workerd::jsg