Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions rivetkit-rust/packages/rivetkit/src/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ impl<A: Actor> Input<A> {
}

pub fn decode(&self) -> Result<A::Input> {
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::<A::Input>() == TypeId::of::<()>() => {
let unit: Box<dyn Any> = Box::new(());
Expand All @@ -62,12 +66,18 @@ impl<A: Actor> Input<A> {
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<A::Input>
where
A::Input: Default,
Expand Down Expand Up @@ -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::<DefaultActor> {
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::<EmptyActor> {
bytes: Some(Vec::new()),
_p: PhantomData,
};

assert_eq!(input.decode().expect("empty unit input"), ());
}

#[test]
fn connection_params_decode_null_as_default() {
assert_eq!(
Expand Down Expand Up @@ -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::<LifecycleActor>(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();
Expand Down
Loading