clockworklabs/SpacetimeDB · error
expected reducer transaction update {request_id}, got reques
Error message
expected reducer transaction update {request_id}, got request id {:?} What it means
Test-harness helper recv_reducer_update(request_id) in crates/testing/src/modules.rs:145 pops exactly one message from the client's outbound stream and requires it to be a full TxUpdate whose event.request_id equals the given RequestId. The outbound stream is FIFO across all traffic (other reducers, subscription updates, energy updates), so if the next message belongs to a different transaction, the helper bails with this mismatch error. It is a strict single-message assertion, not a search.
Source
Thrown at crates/testing/src/modules.rs:145
pub async fn recv_message(&mut self) -> Option<OutboundMessage> {
let mut buf = Vec::with_capacity(1);
(self.receiver.recv_many(&mut buf, 1).await != 0).then(|| buf.remove(0))
}
pub async fn recv_reducer_update(&mut self, request_id: RequestId) -> anyhow::Result<()> {
let message = self
.recv_message()
.await
.ok_or_else(|| anyhow::anyhow!("client receiver closed before reducer update {request_id}"))?;
let OutboundMessage::V1(SerializableMessage::TxUpdate(update)) = message else {
anyhow::bail!("expected reducer transaction update {request_id}, got {message:?}");
};
let Some(event) = update.event else {
anyhow::bail!("expected full reducer transaction update {request_id}, got light update");
};
if event.request_id != Some(request_id) {
anyhow::bail!(
"expected reducer transaction update {request_id}, got request id {:?}",
event.request_id
);
}
match &event.status {
EventStatus::Committed(_) => Ok(()),
EventStatus::FailedUser(err) | EventStatus::FailedInternal(err) => {
anyhow::bail!("reducer transaction update {request_id} failed: {err}")
}
EventStatus::OutOfEnergy => anyhow::bail!("reducer transaction update {request_id} ran out of energy"),
}
}
pub async fn read_log(&self, size: Option<u32>) -> String {
let bytes = self
.client
.module()
.database_logger()View on GitHub (pinned to fb7282411b)
Solutions
- Call recv_reducer_update(request_id) immediately after the exact call_reducer* call that returned that request_id, before triggering any other transaction on the connection.
- Consume preceding unrelated updates first with recv_message() in a loop until you reach the TxUpdate whose event.request_id matches.
- Verify the RequestId comes from the same exec/call - the call_reducer_binary_result helper at crates/testing/src/modules.rs:104 returns it.
- If tests genuinely interleave transactions, write a small dispatcher that routes each TxUpdate by request_id instead of assuming order.
Example fix
// before: unrelated update arrives first, mismatch is fatal
let rid = module.call_reducer_binary_result("add", &args).await?;
module.recv_reducer_update(rid).await?; // bails if another TxUpdate is queued ahead
// after: drain until the matching update
while let Some(msg) = module.recv_message().await {
if let OutboundMessage::V1(SerializableMessage::TxUpdate(u)) = &msg {
if u.event.as_ref().and_then(|e| e.request_id) == Some(rid) {
break; // found ours; now assert on its status
}
}
} Defensive patterns
Strategy: try-catch
Try / catch
// Rust test: route updates by request_id instead of assuming FIFO position.
loop {
let Some(msg) = module.recv_message().await else { panic!("client closed") };
let OutboundMessage::V1(SerializableMessage::TxUpdate(u)) = msg else { continue };
let Some(ev) = u.event else { continue };
if ev.request_id == Some(rid) {
assert!(matches!(ev.status, EventStatus::Committed(_)), "status: {:?}", ev.status);
break;
}
} Prevention
- Await recv_reducer_update immediately after the exact call_reducer* that returned the request_id, before starting any other transaction.
- Always bind the RequestId from the call's return value; never hand-copy ids between tests.
- In helpers that call reducers, consume or return the update so callers cannot get out of sync with the message stream.
When it happens
Trigger: Calling recv_reducer_update(rid_a) when an earlier transaction (another reducer call, a subscription initial load, a second client's write) produced an update that has not been consumed yet; passing a RequestId captured from a different call_reducer invocation; firing two reducers back-to-back and awaiting them in reverse order.
Common situations: Test modules that start subscriptions and then call reducers without draining the subscription's initial data first; helper functions that call reducers internally without awaiting their updates; copy-pasting request ids between tests.
Related errors
- cannot serialize refs without a typespace
- cannot deserialize refs without a typespace
- database is not open
- begin mutable transaction while one is already active
- insert without active mutable transaction
AI-assisted analysis of clockworklabs/SpacetimeDB@fb7282411b (2026-08-20).
Data as JSON: /api/errors/66895befadb0477c.
Report an issue: GitHub.