Skip to content

Commit b68407d

Browse files
committed
Add "wderrid" error IDs to opaque "internal error" exceptions
...to make it easier to correlate internal error logs with user error reports.
1 parent 2e2a0cd commit b68407d

File tree

3 files changed

+99
-18
lines changed

3 files changed

+99
-18
lines changed

src/workerd/jsg/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ wd_cc_library(
7373
"//src/workerd/util:uuid",
7474
"@capnp-cpp//src/kj",
7575
"@simdutf",
76+
"@ssl",
7677
"@workerd-v8//:v8",
7778
],
7879
)

src/workerd/jsg/util-test.c++

+33-10
Original file line numberDiff line numberDiff line change
@@ -113,25 +113,36 @@ struct ThrowContext: public ContextGlobalObject {
113113
JSG_DECLARE_ISOLATE_TYPE(ThrowIsolate, ThrowContext);
114114

115115
KJ_TEST("throw internal error") {
116+
setPredictableModeForTest();
117+
116118
Evaluator<ThrowContext, ThrowIsolate> e(v8System);
117119
{
118120
KJ_EXPECT_LOG(ERROR, "thrown from throwException");
119-
e.expectEval("throwException()", "throws", "Error: internal error");
121+
e.expectEval(
122+
"throwException()", "throws", "Error: internal error; reference = 0123456789abcdefghijklmn");
123+
}
124+
{
125+
// We also expect the logged internal error to contain the error id.
126+
KJ_EXPECT_LOG(ERROR, "wdErrId = 0123456789abcdefghijklmn");
127+
e.expectEval(
128+
"throwException()", "throws", "Error: internal error; reference = 0123456789abcdefghijklmn");
120129
}
121130

122131
{
123132
KJ_EXPECT_LOG(ERROR, "thrown from getThrowing");
124-
e.expectEval("throwing", "throws", "Error: internal error");
133+
e.expectEval("throwing", "throws", "Error: internal error; reference = 0123456789abcdefghijklmn");
125134
}
126135

127136
{
128137
KJ_EXPECT_LOG(ERROR, "thrown from setThrowing");
129-
e.expectEval("throwing = 123", "throws", "Error: internal error");
138+
e.expectEval(
139+
"throwing = 123", "throws", "Error: internal error; reference = 0123456789abcdefghijklmn");
130140
}
131141

132142
{
133143
KJ_EXPECT_LOG(ERROR, "thrown from returnFunctionThatThrows");
134-
e.expectEval("returnFunctionThatThrows(123)(321)", "throws", "Error: internal error");
144+
e.expectEval("returnFunctionThatThrows(123)(321)", "throws",
145+
"Error: internal error; reference = 0123456789abcdefghijklmn");
135146
}
136147
}
137148

@@ -262,6 +273,8 @@ struct TunneledContext: public ContextGlobalObject {
262273
JSG_DECLARE_ISOLATE_TYPE(TunneledIsolate, TunneledContext);
263274

264275
KJ_TEST("throw tunneled exception") {
276+
setPredictableModeForTest();
277+
265278
Evaluator<TunneledContext, TunneledIsolate> e(v8System);
266279
e.expectEval(
267280
"throwTunneledTypeError()", "throws", "TypeError: thrown from throwTunneledTypeError");
@@ -276,16 +289,24 @@ KJ_TEST("throw tunneled exception") {
276289
"OperationError: thrown from throwTunneledOperationErrorWithExpectation");
277290
{
278291
KJ_EXPECT_LOG(ERROR, "thrown from throwTunneledInternalOperationError");
279-
e.expectEval(
280-
"throwTunneledInternalOperationError()", "throws", "OperationError: internal error");
292+
e.expectEval("throwTunneledInternalOperationError()", "throws",
293+
"OperationError: internal error; reference = 0123456789abcdefghijklmn");
294+
}
295+
{
296+
// We also expect the logged internal error to contain the error id.
297+
KJ_EXPECT_LOG(ERROR, "wdErrId = 0123456789abcdefghijklmn");
298+
e.expectEval("throwTunneledInternalOperationError()", "throws",
299+
"OperationError: internal error; reference = 0123456789abcdefghijklmn");
281300
}
282301
{
283302
KJ_EXPECT_LOG(ERROR, " jsg.TypeError");
284-
e.expectEval("throwBadTunneledError()", "throws", "Error: internal error");
303+
e.expectEval("throwBadTunneledError()", "throws",
304+
"Error: internal error; reference = 0123456789abcdefghijklmn");
285305
}
286306
{
287307
KJ_EXPECT_LOG(ERROR, "expected s.startsWith(\";\"); jsg.TypeError");
288-
e.expectEval("throwBadTunneledErrorWithExpectation()", "throws", "Error: internal error");
308+
e.expectEval("throwBadTunneledErrorWithExpectation()", "throws",
309+
"Error: internal error; reference = 0123456789abcdefghijklmn");
289310
}
290311
e.expectEval("throwTunneledMacroTypeError()", "throws",
291312
"TypeError: thrown from throwTunneledMacroTypeError");
@@ -305,11 +326,13 @@ KJ_TEST("throw tunneled exception") {
305326
"throwTunneledDOMException()", "throws", "Some error: thrown from throwTunneledDOMException");
306327
{
307328
KJ_EXPECT_LOG(ERROR, " thrown from throwTunneledInvalidDOMException");
308-
e.expectEval("throwTunneledInvalidDOMException()", "throws", "Error: internal error");
329+
e.expectEval("throwTunneledInvalidDOMException()", "throws",
330+
"Error: internal error; reference = 0123456789abcdefghijklmn");
309331
}
310332
{
311333
KJ_EXPECT_LOG(ERROR, " thrown from throwTunneledGarbledDOMException");
312-
e.expectEval("throwTunneledGarbledDOMException()", "throws", "Error: internal error");
334+
e.expectEval("throwTunneledGarbledDOMException()", "throws",
335+
"Error: internal error; reference = 0123456789abcdefghijklmn");
313336
}
314337
}
315338

src/workerd/jsg/util.c++

+65-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "ser.h"
77
#include "setup.h"
88

9+
#include <openssl/rand.h>
10+
911
#include <kj/debug.h>
1012

1113
#include <cstdlib>
@@ -90,9 +92,53 @@ kj::String typeName(const std::type_info& type) {
9092
return kj::mv(result);
9193
}
9294

95+
namespace {
96+
97+
// For internal errors, we generate an ID to include when rendering user-facing "internal error"
98+
// exceptions and writing internal exception logs, to make it easier to search for logs
99+
// corresponding to "internal error" exceptions reported by users.
100+
//
101+
// We'll use an ID of 24 base-32 encoded characters, just because its relatively simple to
102+
// generate from random bytes. This should give us a value with 120 bits of uniqueness, which is
103+
// about as good as a UUID.
104+
//
105+
// (We're not using base-64 encoding to avoid issues with case insensitive search, as well as
106+
// ensuring that the id is easy to select and copy via double-clicking.)
107+
using InternalErrorId = kj::FixedArray<char, 24>;
108+
109+
constexpr char BASE32_DIGITS[] = "0123456789abcdefghijklmnopqrstuv";
110+
111+
InternalErrorId makeInternalErrorId() {
112+
InternalErrorId id;
113+
if (isPredictableModeForTest()) {
114+
// In testing mode, use content that generates a "0123456789abcdefghijklm" ID:
115+
for (auto i: kj::indices(id)) {
116+
id[i] = i;
117+
}
118+
} else {
119+
KJ_ASSERT(RAND_bytes(id.asPtr().asBytes().begin(), id.size()) == 1);
120+
}
121+
for (auto i: kj::indices(id)) {
122+
id[i] = BASE32_DIGITS[static_cast<unsigned char>(id[i]) % 32];
123+
}
124+
return id;
125+
}
126+
127+
kj::String renderInternalError(InternalErrorId& internalErrorId) {
128+
// TODO(now): put "internal error" change behind autogate or compatibility flag?
129+
//
130+
// It's possible that existing user error handling systems could rely on an exact match with the
131+
// existing "internal error" string. On the other hand, feature flags need a jsg::Lock to read,
132+
// which may not be available in all contexts that generate internal errors?
133+
return kj::str("internal error; reference = ", internalErrorId);
134+
}
135+
136+
} // namespace
137+
93138
v8::Local<v8::Value> makeInternalError(v8::Isolate* isolate, kj::StringPtr internalMessage) {
94-
KJ_LOG(ERROR, internalMessage);
95-
return v8::Exception::Error(v8StrIntern(isolate, "internal error"));
139+
auto wdErrId = makeInternalErrorId();
140+
KJ_LOG(ERROR, internalMessage, wdErrId);
141+
return v8::Exception::Error(v8StrIntern(isolate, renderInternalError(wdErrId)));
96142
}
97143

98144
namespace {
@@ -143,6 +189,8 @@ struct DecodedException {
143189
bool isInternal;
144190
bool isFromRemote;
145191
bool isDurableObjectReset;
192+
// TODO(cleanup): Maybe<> is redundant with isInternal flag field?
193+
kj::Maybe<InternalErrorId> internalErrorId;
146194
};
147195

148196
DecodedException decodeTunneledException(
@@ -167,15 +215,17 @@ DecodedException decodeTunneledException(
167215
// TODO(someday): Support arbitrary user-defined error types, not just Error?
168216
auto tunneledInfo = tunneledErrorType(internalMessage);
169217

218+
DecodedException result;
170219
auto errorType = tunneledInfo.message;
171-
auto appMessage = [&](kj::StringPtr errorString) -> kj::StringPtr {
220+
auto appMessage = [&](kj::StringPtr errorString) -> kj::ConstString {
172221
if (tunneledInfo.isInternal) {
173-
return "internal error"_kj;
222+
result.internalErrorId = makeInternalErrorId();
223+
return kj::ConstString(renderInternalError(KJ_ASSERT_NONNULL(result.internalErrorId)));
174224
} else {
175-
return trimErrorMessage(errorString);
225+
// .attach() to convert StringPtr to ConstString:
226+
return trimErrorMessage(errorString).attach();
176227
}
177228
};
178-
DecodedException result;
179229
result.isInternal = tunneledInfo.isInternal;
180230
result.isFromRemote = tunneledInfo.isFromRemote;
181231
result.isDurableObjectReset = tunneledInfo.isDurableObjectReset;
@@ -213,7 +263,9 @@ DecodedException decodeTunneledException(
213263
}
214264
}
215265
// unrecognized exception type
216-
result.handle = v8::Exception::Error(v8StrIntern(isolate, "internal error"));
266+
result.internalErrorId = makeInternalErrorId();
267+
result.handle = v8::Exception::Error(
268+
v8Str(isolate, renderInternalError(KJ_ASSERT_NONNULL(result.internalErrorId))));
217269
result.isInternal = true;
218270
} while (false);
219271
#undef HANDLE_V8_ERROR
@@ -240,6 +292,7 @@ DecodedException decodeTunneledException(
240292
kj::StringPtr extractTunneledExceptionDescription(kj::StringPtr message) {
241293
auto tunneledError = tunneledErrorType(message);
242294
if (tunneledError.isInternal) {
295+
// TODO(soon): Include an internal error ID in message, and also return the id.
243296
return "Error: internal error";
244297
} else {
245298
return tunneledError.message;
@@ -271,7 +324,11 @@ v8::Local<v8::Value> makeInternalError(v8::Isolate* isolate, kj::Exception&& exc
271324
// DISCONNECTED exceptions as these are unlikely to represent bugs worth tracking.
272325
if (exception.getType() != kj::Exception::Type::DISCONNECTED &&
273326
!isDoNotLogException(exception.getDescription())) {
274-
LOG_EXCEPTION("jsgInternalError", exception);
327+
// LOG_EXCEPTION("jsgInternalError", ...), but with internal error ID:
328+
auto& e = exception;
329+
constexpr auto sentryErrorContext = "jsgInternalError";
330+
auto& wdErrId = KJ_ASSERT_NONNULL(tunneledException.internalErrorId);
331+
KJ_LOG(ERROR, e, sentryErrorContext, wdErrId);
275332
} else {
276333
KJ_LOG(INFO, exception); // Run with --verbose to see exception logs.
277334
}

0 commit comments

Comments
 (0)