Skip to content
File

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

6.2 KB
1// Copyright (c) 2023 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 "exception.h"
6 
7#include <kj/debug.h>
8 
9namespace workerd::jsg {
10 
11kj::StringPtr stripRemoteExceptionPrefix(kj::StringPtr internalMessage) {
12 while (internalMessage.startsWith("remote exception: "_kj)) {
13 // Exception was passed over RPC.
14 internalMessage = internalMessage.slice("remote exception: "_kj.size());
15 }
16 return internalMessage;
17}
18 
19namespace {
20constexpr auto ERROR_PREFIX_DELIM = "; "_kj;
21constexpr auto ERROR_REMOTE_PREFIX = "remote."_kj;
22constexpr auto ERROR_TUNNELED_PREFIX_JSG = "jsg."_kj;
23constexpr auto ERROR_INTERNAL_SOURCE_PREFIX_JSG = "jsg-internal."_kj;
24} // namespace
25 
26TunneledErrorType tunneledErrorType(kj::StringPtr internalMessage) {
27 // A tunneled error in an internal message is prefixed by one of the following patterns,
28 // anchored at the beginning of the message:
29 // jsg.
30 // expected <...>; jsg.
31 // broken.<...>; jsg.
32 // where <...> is some failed expectation from e.g. a KJ_REQUIRE.
33 //
34 // A tunneled error might have a prefix "remote.". This indicates it was tunneled from an actor or
35 // from one worker to another. If this prefix is present, we set `isFromRemote` to true, remove
36 // the "remote." prefix, and continue processing the rest of the error.
37 //
38 // Additionally, a prefix of `jsg-internal.` instead of `jsg.` means "throw a specific
39 // JavaScript error type, but still hide the message text from the app".
40 
41 internalMessage = stripRemoteExceptionPrefix(internalMessage);
42 
43 struct Properties {
44 bool isFromRemote = false;
45 bool isDurableObjectReset = false;
46 bool isDoNotLogException = false;
47 };
48 Properties properties;
49 
50 properties.isDoNotLogException = isDoNotLogException(internalMessage);
51 
52 // Remove `remote.` (if present). Note that there are cases where we return a tunneled error
53 // through multiple workers, so let's be paranoid and allow for multiple "remote." prefixes.
54 while (internalMessage.startsWith(ERROR_REMOTE_PREFIX)) {
55 properties.isFromRemote = true;
56 internalMessage = internalMessage.slice(ERROR_REMOTE_PREFIX.size());
57 }
58 
59 auto findDelim = [](kj::StringPtr msg) -> size_t {
60 // Either return 0 if no matches or the index past the first delim if there are.
61 KJ_IF_SOME(i, msg.find(ERROR_PREFIX_DELIM)) {
62 return i + ERROR_PREFIX_DELIM.size();
63 }
64 return 0;
65 };
66 
67 auto tryExtractError = [](kj::StringPtr msg,
68 Properties properties) -> kj::Maybe<TunneledErrorType> {
69 if (msg.startsWith(ERROR_TUNNELED_PREFIX_JSG)) {
70 return TunneledErrorType{
71 .message = msg.slice(ERROR_TUNNELED_PREFIX_JSG.size()),
72 .isJsgError = true,
73 .isInternal = false,
74 .isFromRemote = properties.isFromRemote,
75 .isDurableObjectReset = properties.isDurableObjectReset,
76 .isDoNotLogException = properties.isDoNotLogException,
77 };
78 }
79 if (msg.startsWith(ERROR_INTERNAL_SOURCE_PREFIX_JSG)) {
80 return TunneledErrorType{
81 .message = msg.slice(ERROR_INTERNAL_SOURCE_PREFIX_JSG.size()),
82 .isJsgError = true,
83 .isInternal = true,
84 .isFromRemote = properties.isFromRemote,
85 .isDurableObjectReset = properties.isDurableObjectReset,
86 .isDoNotLogException = properties.isDoNotLogException,
87 };
88 }
89 
90 return kj::none;
91 };
92 
93 auto makeDefaultError = [](kj::StringPtr msg, Properties properties) {
94 return TunneledErrorType{
95 .message = msg,
96 .isJsgError = false,
97 .isInternal = true,
98 .isFromRemote = properties.isFromRemote,
99 .isDurableObjectReset = properties.isDurableObjectReset,
100 .isDoNotLogException = isDoNotLogException(msg),
101 };
102 };
103 
104 if (internalMessage.startsWith("expected ")) {
105 // This was a test assertion, peel away delimiters until either we find an error or there are
106 // none left.
107 auto idx = findDelim(internalMessage);
108 while (idx) {
109 internalMessage = internalMessage.slice(idx);
110 KJ_IF_SOME(e, tryExtractError(internalMessage, properties)) {
111 return kj::mv(e);
112 }
113 idx = findDelim(internalMessage);
114 }
115 
116 // We failed to extract an expected error, make a default one.
117 return makeDefaultError(internalMessage, properties);
118 }
119 
120 while (internalMessage.startsWith("broken.")) {
121 properties.isDurableObjectReset = true;
122 
123 // Trim away all broken prefixes, they are not allowed to have internal delimiters.
124 internalMessage = internalMessage.slice(findDelim(internalMessage));
125 }
126 
127 // There are no prefixes left, just try to extract the error.
128 KJ_IF_SOME(e, tryExtractError(internalMessage, properties)) {
129 return kj::mv(e);
130 } else {
131 return makeDefaultError(internalMessage, properties);
132 }
133}
134 
135bool isTunneledException(kj::StringPtr internalMessage) {
136 return !tunneledErrorType(internalMessage).isInternal;
137}
138 
139bool isDoNotLogException(kj::StringPtr internalMessage) {
140 return internalMessage.contains("worker_do_not_log"_kjc);
141}
142 
143kj::String annotateBroken(kj::StringPtr internalMessage, kj::StringPtr brokennessReason) {
144 // TODO(soon) Once we support multiple brokenness reasons, we can make this much simpler.
145 
146 KJ_LOG(INFO, "Annotating with brokenness", internalMessage, brokennessReason);
147 auto tunneledInfo = tunneledErrorType(internalMessage);
148 
149 kj::StringPtr remotePrefix;
150 if (tunneledInfo.isFromRemote) {
151 remotePrefix = ERROR_REMOTE_PREFIX;
152 }
153 
154 kj::StringPtr prefixType = ERROR_TUNNELED_PREFIX_JSG;
155 kj::StringPtr internalErrorType;
156 if (tunneledInfo.isInternal) {
157 prefixType = ERROR_INTERNAL_SOURCE_PREFIX_JSG;
158 if (!tunneledInfo.isJsgError) {
159 // This is not a JSG error, so we need to give it a type.
160 internalErrorType = "Error: "_kj;
161 }
162 }
163 
164 return kj::str(remotePrefix, brokennessReason, ERROR_PREFIX_DELIM, prefixType, internalErrorType,
165 tunneledInfo.message);
166}
167 
168bool isExceptionFromInputGateBroken(kj::StringPtr description) {
169 // annotateBroken() produces "broken.inputGateBroken; {message}", optionally prefixed with
170 // "remote." when crossing RPC boundaries. Strip the remote prefix first, then check the tag.
171 return stripRemoteExceptionPrefix(description).startsWith("broken.inputGateBroken; "_kj);
172}
173 
174} // namespace workerd::jsg