-
Notifications
You must be signed in to change notification settings - Fork 290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add exception stack traces to trace/tail events. #1910
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,8 +148,12 @@ void addExceptionToTrace(jsg::Lock& js, | |
} | ||
|
||
auto timestamp = ioContext.now(); | ||
auto error = KJ_REQUIRE_NONNULL(errorTypeHandler.tryUnwrap(js, exception), | ||
"Should always be possible to unwrap error interface from an object."); | ||
Worker::Api::ErrorInterface error; | ||
|
||
if (exception.isObject()) { | ||
error = KJ_REQUIRE_NONNULL(errorTypeHandler.tryUnwrap(js, exception), | ||
"Should always be possible to unwrap error interface from an object."); | ||
} | ||
|
||
kj::String name; | ||
KJ_IF_SOME(n, error.name) { | ||
|
@@ -160,9 +164,49 @@ void addExceptionToTrace(jsg::Lock& js, | |
kj::String message; | ||
KJ_IF_SOME(m, error.message) { | ||
message = kj::str(m); | ||
} else { | ||
// This doesn't appear to be an Error object. Fall back to stringifying the whole value as | ||
// the message. | ||
if (!js.v8Isolate->IsExecutionTerminating()) { | ||
v8::TryCatch tryCatch(js.v8Isolate); | ||
try { | ||
message = exception.toString(js); | ||
} catch (jsg::JsExceptionThrown&) { | ||
// Failed to stringify. | ||
// | ||
// Note that we're intentionally not checking tryCatch.CanContinue() here, because we still | ||
// want to continue even if the isolate has been terminated. | ||
} | ||
} | ||
} | ||
|
||
kj::Maybe<kj::String> stack; | ||
KJ_IF_SOME(s, error.stack) { | ||
kj::StringPtr slice = s; | ||
|
||
// Normally `error.stack` repeats the error type and message first. We don't want send two | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @kentonv — see internal thread on this, I think by stripping out here to be efficient in what gets sent across, we ended up making it such that the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know which internal thread you mean, and I don't see how the code here could possibly be stripping any part of the actual stack trace. |
||
// copies of that to the trace so we'll strip it off. | ||
if (slice.startsWith(name)) { | ||
slice = slice.slice(name.size()); | ||
if (slice.startsWith(": "_kj)) { | ||
slice = slice.slice(2); | ||
} | ||
} | ||
|
||
if (slice.startsWith(message)) { | ||
slice = slice.slice(message.size()); | ||
if (slice.startsWith("\n")) { | ||
slice = slice.slice(1); | ||
} | ||
} | ||
|
||
if (slice.size() > 0) { | ||
stack = kj::str(slice); | ||
} | ||
} | ||
|
||
// TODO(someday): Limit size of exception content? | ||
tracer.addException(timestamp, kj::mv(name), kj::mv(message)); | ||
tracer.addException(timestamp, kj::mv(name), kj::mv(message), kj::mv(stack)); | ||
} | ||
|
||
void reportStartupError( | ||
|
@@ -1839,6 +1883,42 @@ void Worker::Lock::logUncaughtException(UncaughtExceptionSource source, | |
} | ||
} | ||
|
||
void Worker::Lock::logUncaughtException(UncaughtExceptionSource source, | ||
kj::Exception&& exception) { | ||
jsg::Lock& js = *this; | ||
|
||
// If we have an attached serialized exception, deserialize it and log that instead, rather | ||
// than try to reconstruct based on the KJ exception description. | ||
// | ||
// TODO(cleanup): Eventually, js.exceptionToJsValue() should do this internally, and then we | ||
// should remove the code from here. | ||
KJ_IF_SOME(serializedJsError, exception.getDetail(jsg::TUNNELED_EXCEPTION_DETAIL_ID)) { | ||
if (!js.v8Isolate->IsExecutionTerminating()) { | ||
kj::Maybe<jsg::JsValue> deserialized; | ||
|
||
v8::TryCatch tryCatch(js.v8Isolate); | ||
try { | ||
jsg::Deserializer deser(js, serializedJsError); | ||
deserialized = deser.readValue(js); | ||
} catch (jsg::JsExceptionThrown&) { | ||
// Failed to deserialize, we'll continue with exceptionToJsValue() instead. | ||
// | ||
// Note that we're intentionally not checking tryCatch.CanContinue() here, because we still | ||
// want to log the exception even if the isolate has been terminated. | ||
} | ||
|
||
KJ_IF_SOME(d, deserialized) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, |
||
logUncaughtException(source, d); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
// Couldn't deserialize an attached exception, so use `exceptionToJsValue()`. | ||
auto jsError = js.exceptionToJsValue(kj::mv(exception)); | ||
logUncaughtException(source, jsError.getHandle(js)); | ||
} | ||
|
||
void Worker::Lock::reportPromiseRejectEvent(v8::PromiseRejectMessage& message) { | ||
getGlobalScope().emitPromiseRejection( | ||
*this, message.GetEvent(), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
|
||
#include "jsg.h" // can't include util.h directly due to weird cyclic dependency... | ||
#include "setup.h" | ||
#include "ser.h" | ||
#include <kj/debug.h> | ||
#include <stdlib.h> | ||
|
||
|
@@ -265,6 +266,15 @@ kj::StringPtr extractTunneledExceptionDescription(kj::StringPtr message) { | |
|
||
v8::Local<v8::Value> makeInternalError(v8::Isolate* isolate, kj::Exception&& exception) { | ||
auto desc = exception.getDescription(); | ||
|
||
// TODO(someday): Deserialize encoded V8 exception from | ||
// exception.getDetail(TUNNELED_EXCEPTION_DETAIL_ID), if present. WARNING: We must think | ||
// carefully about security in the case that the exception has passed between workers that | ||
// don't trust each other. Perhaps we should explicitly remove the stack trace in this case. | ||
// REMINDER: Worker::logUncaughtException() currently deserializes TUNNELED_EXCEPTION_DETAIL_ID | ||
// in order to extract a full stack trace. Once we do it here, we can remove the code from | ||
// there. | ||
|
||
auto tunneledException = decodeTunneledException(isolate, desc); | ||
|
||
if (tunneledException.isInternal) { | ||
|
@@ -341,6 +351,20 @@ void throwInternalError(v8::Isolate* isolate, kj::Exception&& exception) { | |
} | ||
} | ||
|
||
void addExceptionDetail(Lock& js, kj::Exception& exception, v8::Local<v8::Value> handle) { | ||
js.tryCatch([&]() { | ||
Serializer ser(js, { | ||
// Make sure we don't break compatibility if V8 introduces a new version. This vaule can | ||
// be bumped to match the new version once all of production is updated to understand it. | ||
.version = 15, | ||
}); | ||
ser.write(js, JsValue(handle)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be doing anything to bound the amount of data added to an exception? Edit: Oh, I guess the existing trace size bound would help limit negative effects, and I suppose V8 might have some internal bound on stack trace generation. |
||
exception.setDetail(TUNNELED_EXCEPTION_DETAIL_ID, ser.release().data); | ||
}, [](jsg::Value&& error) { | ||
// Exception not serializable, ignore. | ||
}); | ||
} | ||
|
||
static kj::String typeErrorMessage(TypeErrorContext c, const char* expectedType) { | ||
kj::String type; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the
isObject()
check is needed here because the deserialized result isn't necessarily an Object? (But would typically be an Object?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoyingly, JavaScript permits you to throw any value, not just objects. I think there was a bug here that caused us to fail to log non-object exceptions, but it wasn't noticed before because our exception tunneling would turn non-objects into objects. (Exception tunneling today always produces
Error
values even if the original wasn't.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, OK... that would explain why the current test cases for
throw "string"
were working without the Object check.