linera-io/linera-protocol · error · ExecutionError
ServiceOracleQueryOperations
ServiceOracleQueryOperations
Error message
ExecutionError::ServiceOracleQueryOperations(operations)
What it means
When a contract queries a service as an oracle (QueryServiceOracle), the query must be side-effect free with respect to consensus: the QueryOutcome may carry a response but must contain zero operations. If the service handler schedules operations during the query, the actor fails with ServiceOracleQueryOperations listing the offending operations (execution_state_actor.rs:750), because oracle responses must replay deterministically.
Source
Thrown at linera-execution/src/execution_state_actor.rs:750
.txn_tracker
.oracle(|| async {
let context = QueryContext {
chain_id: state.context().extra().chain_id(),
next_block_height,
local_time,
};
let QueryOutcome {
response,
operations,
} = Box::pin(state.query_user_application_with_deadline(
application_id,
context,
query,
deadline,
created_blobs,
))
.await?;
ensure!(
operations.is_empty(),
ExecutionError::ServiceOracleQueryOperations(operations)
);
Ok(OracleResponse::Service(response))
})
.await?
.to_service_response()?;
callback.respond(bytes);
}
AddOutgoingMessage { message, callback } => {
self.txn_tracker.add_outgoing_message(message);
callback.respond(());
}
SetLocalTime {
local_time,
callback,View on GitHub (pinned to 6c226ddcb3)
Solutions
- Remove operation-scheduling from the service query handler; queries may only read state and return bytes
- If an action is required, have the calling contract emit the operation itself instead of delegating to the service
- Add a unit test asserting QueryOutcome.operations is empty for every query path of the application
Example fix
// before
fn handle_query(q: Query) -> Result<Response> {
if q.should_act { self.schedule_operation(op)?; }
self.read_state(q)
}
// after
fn handle_query(q: Query) -> Result<Response> {
// read-only: never schedule operations here
self.read_state(q)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Test-level validation: every query path must produce zero operations
#[test]
fn queries_have_no_side_effects() {
for query in test_queries() {
let QueryOutcome { response: _, operations } = service.handle_query(query);
assert!(operations.is_empty(), "query scheduled operations");
}
} Type guard
fn is_service_oracle_query_operations(err: &ExecutionError) -> bool {
matches!(err, ExecutionError::ServiceOracleQueryOperations(_))
} Try / catch
match contract.call_service_oracle(app_id, query).await {
Ok(bytes) => bytes,
Err(ref e) if is_service_oracle_query_operations(e) => {
// service bug: query handler scheduled operations; fix the service, do not retry
return Err(anyhow!("service query must not schedule operations; fix its query handler"));
}
Err(e) => return Err(e.into()),
} Prevention
- Keep query handlers strictly read-only; put all state changes in operation and message handlers
- Add a unit test asserting QueryOutcome.operations is empty for every query variant
- Review framework hooks so nothing auto-schedules operations during query execution
When it happens
Trigger: A service's query handler calls a runtime API that records operations (schedule or add operation) while the service is being queried as an oracle by a contract.
Common situations: One code path shared between query and act in an application; services ported from contract-style code that always enqueue follow-up operations; framework hooks that auto-schedule during any handler; forgetting that oracle queries are read-only.
Related errors
- MaximumServiceOracleExecutionTimeExceeded
- ServiceOracleResponseTooLarge
- no signer found for owner ${owner}
- Invalid owner address
- Invalid owner address
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/9f5e2027a64dc5a9.
Report an issue: GitHub.