diff --git a/rivetkit-rust/packages/rivetkit/src/start.rs b/rivetkit-rust/packages/rivetkit/src/start.rs index faa5368c67..a6ee050cf6 100644 --- a/rivetkit-rust/packages/rivetkit/src/start.rs +++ b/rivetkit-rust/packages/rivetkit/src/start.rs @@ -46,7 +46,11 @@ impl Input { } pub fn decode(&self) -> Result { - match self.bytes.as_deref() { + // Treat empty input bytes as absent so a zero-length payload defaults + // like a missing one, mirroring snapshot decoding. The engine encodes + // an omitted `input` as no bytes, but an empty base64 string decodes to + // an empty (non-null) buffer. + match self.present_bytes() { Some(bytes) => decode_cbor(bytes, "actor input"), None if TypeId::of::() == TypeId::of::<()>() => { let unit: Box = Box::new(()); @@ -62,12 +66,18 @@ impl Input { where F: FnOnce() -> A::Input, { - match self.bytes.as_deref() { + match self.present_bytes() { Some(bytes) => decode_cbor(bytes, "actor input"), None => Ok(f()), } } + /// Input bytes with empty buffers normalized to `None`, so a zero-length + /// payload is treated the same as an omitted one. + fn present_bytes(&self) -> Option<&[u8]> { + self.bytes.as_deref().filter(|bytes| !bytes.is_empty()) + } + pub fn decode_or_default(&self) -> Result where A::Input: Default, @@ -746,6 +756,31 @@ mod tests { assert_eq!(input.decode().expect("missing unit input"), ()); } + #[test] + fn input_decode_or_default_treats_empty_bytes_as_missing() { + // An empty base64 input decodes to an empty (non-null) buffer, which + // must default rather than fail CBOR decoding. + let input = Input:: { + bytes: Some(Vec::new()), + _p: PhantomData, + }; + + assert_eq!( + input.decode_or_default().expect("default input"), + DefaultInput { count: 7 } + ); + } + + #[test] + fn input_decode_treats_empty_unit_as_unit() { + let input = Input:: { + bytes: Some(Vec::new()), + _p: PhantomData, + }; + + assert_eq!(input.decode().expect("empty unit input"), ()); + } + #[test] fn connection_params_decode_null_as_default() { assert_eq!( @@ -946,6 +981,22 @@ mod tests { actor.await.expect("join run_actor").expect("run actor"); } + #[tokio::test] + async fn run_actor_invalid_input_fails_to_start() { + // Non-empty bytes that are not valid CBOR for the input type must fail + // the actor start rather than silently defaulting. + let (_tx, rx) = unbounded_channel(); + let start = lifecycle_start(Some(vec![0xff, 0xff, 0xff]), None, rx.into()); + + let error = run_actor::(start) + .await + .expect_err("invalid input should fail actor start"); + assert!( + format!("{error:#}").contains("decode actor input from cbor"), + "unexpected error: {error:#}" + ); + } + #[tokio::test] async fn run_actor_default_websocket_rejects() { let (tx, rx) = unbounded_channel();