{"record":{"id":"9f5e2027a64dc5a9","repo":"linera-io/linera-protocol","slug":"serviceoraclequeryoperations","errorCode":"ServiceOracleQueryOperations","errorMessage":"ExecutionError::ServiceOracleQueryOperations(operations)","messagePattern":"ExecutionError::ServiceOracleQueryOperations\\(operations\\)","errorType":"exception","errorClass":"ExecutionError","httpStatus":null,"severity":"error","filePath":"linera-execution/src/execution_state_actor.rs","lineNumber":750,"sourceCode":"                    .txn_tracker\n                    .oracle(|| async {\n                        let context = QueryContext {\n                            chain_id: state.context().extra().chain_id(),\n                            next_block_height,\n                            local_time,\n                        };\n                        let QueryOutcome {\n                            response,\n                            operations,\n                        } = Box::pin(state.query_user_application_with_deadline(\n                            application_id,\n                            context,\n                            query,\n                            deadline,\n                            created_blobs,\n                        ))\n                        .await?;\n                        ensure!(\n                            operations.is_empty(),\n                            ExecutionError::ServiceOracleQueryOperations(operations)\n                        );\n                        Ok(OracleResponse::Service(response))\n                    })\n                    .await?\n                    .to_service_response()?;\n                callback.respond(bytes);\n            }\n\n            AddOutgoingMessage { message, callback } => {\n                self.txn_tracker.add_outgoing_message(message);\n                callback.respond(());\n            }\n\n            SetLocalTime {\n                local_time,\n                callback,","sourceCodeStart":732,"sourceCodeEnd":768,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-execution/src/execution_state_actor.rs#L732-L768","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfn handle_query(q: Query) -> Result<Response> {\n    if q.should_act { self.schedule_operation(op)?; }\n    self.read_state(q)\n}\n\n// after\nfn handle_query(q: Query) -> Result<Response> {\n    // read-only: never schedule operations here\n    self.read_state(q)\n}","handlingStrategy":"try-catch","validationCode":"// Test-level validation: every query path must produce zero operations\n#[test]\nfn queries_have_no_side_effects() {\n    for query in test_queries() {\n        let QueryOutcome { response: _, operations } = service.handle_query(query);\n        assert!(operations.is_empty(), \"query scheduled operations\");\n    }\n}","typeGuard":"fn is_service_oracle_query_operations(err: &ExecutionError) -> bool {\n    matches!(err, ExecutionError::ServiceOracleQueryOperations(_))\n}","tryCatchPattern":"match contract.call_service_oracle(app_id, query).await {\n    Ok(bytes) => bytes,\n    Err(ref e) if is_service_oracle_query_operations(e) => {\n        // service bug: query handler scheduled operations; fix the service, do not retry\n        return Err(anyhow!(\"service query must not schedule operations; fix its query handler\"));\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["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"],"tags":["service-oracle","read-only","query","side-effects","linera"],"backgroundTag":"side-effects-in-query","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}