Skip to content

Commit b24f3be

Browse files
committed
fix(rivetkit): preserve internal bridge errors
1 parent 395aa83 commit b24f3be

10 files changed

Lines changed: 142 additions & 23 deletions

File tree

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use rivet_error::{MacroMarker, RivetError, RivetErrorSchema};
1+
use rivet_error::{INTERNAL_ERROR, MacroMarker, RivetError, RivetErrorSchema};
22
use serde::{Deserialize, Serialize};
33
use serde_json::Value as JsonValue;
44

@@ -12,30 +12,62 @@ pub struct ActionDispatchError {
1212

1313
impl ActionDispatchError {
1414
pub(crate) fn from_anyhow(error: anyhow::Error) -> Self {
15+
let original_message = error.to_string();
1516
let error = RivetError::extract(&error);
17+
let message = if is_internal_error(error.group(), error.code()) {
18+
original_message
19+
} else {
20+
error.message().to_owned()
21+
};
1622
Self {
1723
group: error.group().to_owned(),
1824
code: error.code().to_owned(),
19-
message: error.message().to_owned(),
25+
message,
2026
metadata: error.metadata(),
2127
}
2228
}
2329

30+
pub(crate) fn client_message(&self) -> &str {
31+
if is_internal_error(&self.group, &self.code) {
32+
INTERNAL_ERROR.default_message
33+
} else {
34+
&self.message
35+
}
36+
}
37+
38+
pub(crate) fn client_metadata(&self) -> Option<&JsonValue> {
39+
if is_internal_error(&self.group, &self.code) {
40+
None
41+
} else {
42+
self.metadata.as_ref()
43+
}
44+
}
45+
2446
pub(crate) fn into_anyhow(self) -> anyhow::Error {
47+
let message = self.client_message().to_owned();
2548
let meta = self
26-
.metadata
27-
.and_then(|value| serde_json::value::to_raw_value(&value).ok());
49+
.client_metadata()
50+
.and_then(|value| serde_json::value::to_raw_value(value).ok());
2851
let schema = Box::leak(Box::new(RivetErrorSchema {
2952
group: Box::leak(self.group.into_boxed_str()),
3053
code: Box::leak(self.code.into_boxed_str()),
31-
default_message: Box::leak(self.message.clone().into_boxed_str()),
54+
default_message: Box::leak(message.clone().into_boxed_str()),
3255
meta_type: None,
3356
_macro_marker: MacroMarker { _private: () },
3457
}));
3558
anyhow::Error::new(RivetError {
3659
schema,
3760
meta,
38-
message: Some(self.message),
61+
message: Some(message),
3962
})
4063
}
4164
}
65+
66+
fn is_internal_error(group: &str, code: &str) -> bool {
67+
(group == "core" || group == "rivetkit") && code == "internal_error"
68+
}
69+
70+
// Test shim keeps moved tests under tests while retaining private module access.
71+
#[cfg(test)]
72+
#[path = "../../tests/modules/action_dispatch_error.rs"]
73+
mod tests;

rivetkit-rust/packages/rivetkit-core/src/registry/actor_connect.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,13 +426,13 @@ pub(super) fn action_dispatch_error_response(
426426
action_id: u64,
427427
) -> ActorConnectError {
428428
let metadata = error
429-
.metadata
430-
.as_ref()
429+
.client_metadata()
431430
.and_then(|metadata| encode_json_as_cbor(metadata).ok().map(ByteBuf::from));
431+
let message = error.client_message().to_owned();
432432
ActorConnectError {
433433
group: error.group,
434434
code: error.code,
435-
message: error.message,
435+
message,
436436
metadata,
437437
action_id: Some(action_id),
438438
}

rivetkit-rust/packages/rivetkit-core/src/registry/http.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -905,8 +905,8 @@ pub(super) fn framework_action_error_response(
905905
encoding,
906906
&error.group,
907907
&error.code,
908-
&error.message,
909-
error.metadata.as_ref(),
908+
error.client_message(),
909+
error.client_metadata(),
910910
)?),
911911
body_stream: None,
912912
})

rivetkit-rust/packages/rivetkit-core/src/registry/inspector.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ pub(super) fn action_error_response(error: ActionDispatchError) -> HttpResponse
655655
} else {
656656
StatusCode::INTERNAL_SERVER_ERROR
657657
};
658-
inspector_error_response(status, &error.group, &error.code, &error.message)
658+
inspector_error_response(status, &error.group, &error.code, error.client_message())
659659
}
660660

661661
pub(super) fn inspector_anyhow_response(error: anyhow::Error) -> HttpResponse {

rivetkit-rust/packages/rivetkit-core/tests/modules/action.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ mod moved_tests {
152152
group: "actor".to_owned(),
153153
code: "action_not_found".to_owned(),
154154
message: "action `missing` was not found".to_owned(),
155+
metadata: None,
155156
}
156157
);
157158
}
@@ -188,11 +189,11 @@ mod moved_tests {
188189
}
189190

190191
#[tokio::test]
191-
async fn dispatch_extracts_group_code_and_message_from_anyhow_errors() {
192+
async fn dispatch_preserves_internal_anyhow_message_until_client_boundary() {
192193
let mut callbacks = ActorInstanceCallbacks::default();
193194
callbacks.actions.insert(
194195
"explode".to_owned(),
195-
action_handler(|_| Box::pin(async move { Err(INTERNAL_ERROR.build()) })),
196+
action_handler(|_| Box::pin(async move { Err(anyhow::anyhow!("plain failure")) })),
196197
);
197198

198199
let invoker = ActionInvoker::new(ActorConfig::default(), callbacks);
@@ -203,7 +204,8 @@ mod moved_tests {
203204

204205
assert_eq!(error.group, "core");
205206
assert_eq!(error.code, "internal_error");
206-
assert_eq!(error.message, "An internal error occurred");
207+
assert_eq!(error.message, "plain failure");
208+
assert_eq!(error.client_message(), "An internal error occurred");
207209
}
208210

209211
#[tokio::test]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use super::*;
2+
3+
#[test]
4+
fn preserves_internal_anyhow_message_until_client_boundary() {
5+
let error = ActionDispatchError::from_anyhow(anyhow::anyhow!("plain failure"));
6+
7+
assert_eq!(error.group, "core");
8+
assert_eq!(error.code, "internal_error");
9+
assert_eq!(error.message, "plain failure");
10+
assert_eq!(error.client_message(), "An internal error occurred");
11+
assert_eq!(error.client_metadata(), None);
12+
}
13+
14+
#[test]
15+
fn preserves_public_error_message_for_client_boundary() {
16+
static TEST_ERROR: RivetErrorSchema = RivetErrorSchema {
17+
group: "actor",
18+
code: "action_not_found",
19+
default_message: "action `missing` was not found",
20+
meta_type: None,
21+
_macro_marker: MacroMarker { _private: () },
22+
};
23+
let error = ActionDispatchError::from_anyhow(TEST_ERROR.build());
24+
25+
assert_eq!(error.group, "actor");
26+
assert_eq!(error.code, "action_not_found");
27+
assert_eq!(error.message, "action `missing` was not found");
28+
assert_eq!(error.client_message(), "action `missing` was not found");
29+
}

rivetkit-rust/packages/rivetkit-core/tests/modules/registry_http.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,33 @@ fn framework_action_error_response_maps_timeout_to_408() {
195195
);
196196
}
197197

198+
#[test]
199+
fn framework_action_error_response_sanitizes_internal_message() {
200+
let response = framework_action_error_response(
201+
HttpResponseEncoding::Json,
202+
ActionDispatchError {
203+
group: "rivetkit".to_owned(),
204+
code: "internal_error".to_owned(),
205+
message: "plain failure".to_owned(),
206+
metadata: Some(json!({ "error": "plain failure" })),
207+
},
208+
)
209+
.expect("internal error response should serialize");
210+
211+
assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR.as_u16());
212+
assert_eq!(
213+
response.body,
214+
Some(
215+
serde_json::to_vec(&json!({
216+
"group": "rivetkit",
217+
"code": "internal_error",
218+
"message": "An internal error occurred",
219+
}))
220+
.expect("json body should encode")
221+
)
222+
);
223+
}
224+
198225
#[test]
199226
fn message_boundary_error_response_defaults_to_json() {
200227
let response = message_boundary_error_response(

rivetkit-rust/packages/rivetkit-core/tests/registry_http.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,33 @@ mod moved_tests {
154154
);
155155
}
156156

157+
#[test]
158+
fn framework_action_error_response_sanitizes_internal_message() {
159+
let response = framework_action_error_response(
160+
HttpResponseEncoding::Json,
161+
ActionDispatchError {
162+
group: "rivetkit".to_owned(),
163+
code: "internal_error".to_owned(),
164+
message: "plain failure".to_owned(),
165+
metadata: Some(json!({ "error": "plain failure" })),
166+
},
167+
)
168+
.expect("internal error response should serialize");
169+
170+
assert_eq!(response.status, StatusCode::INTERNAL_SERVER_ERROR.as_u16());
171+
assert_eq!(
172+
response.body,
173+
Some(
174+
serde_json::to_vec(&json!({
175+
"group": "rivetkit",
176+
"code": "internal_error",
177+
"message": "An internal error occurred",
178+
}))
179+
.expect("json body should encode")
180+
)
181+
);
182+
}
183+
157184
#[test]
158185
fn message_boundary_error_response_defaults_to_json() {
159186
let response = message_boundary_error_response(

rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,16 +1033,13 @@ pub(crate) fn callback_error(callback_name: &str, error: napi::Error) -> anyhow:
10331033
.build();
10341034
}
10351035

1036-
tracing::debug!(
1036+
tracing::warn!(
10371037
callback = callback_name,
10381038
status = ?error.status,
1039+
reason,
10391040
"napi callback failed without structured bridge error prefix"
10401041
);
1041-
JsCallbackUnavailable {
1042-
callback: callback_name.to_owned(),
1043-
reason,
1044-
}
1045-
.build()
1042+
anyhow::anyhow!(reason)
10461043
}
10471044

10481045
impl From<JsActorConfig> for ActorConfigInput {

rivetkit-typescript/packages/rivetkit/src/registry/native.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -670,9 +670,14 @@ function isStructuredBridgeError(
670670
function encodeNativeCallbackError(error: unknown): Error {
671671
const structuredError = isStructuredBridgeError(error)
672672
? error
673-
: deconstructError(error, logger(), {
673+
: deconstructError(
674+
error,
675+
logger(),
676+
{
674677
bridge: "native_callback",
675-
});
678+
},
679+
true,
680+
);
676681
let stack: string | undefined;
677682
if (error instanceof Error) {
678683
try {

0 commit comments

Comments
 (0)