{"record":{"id":"fee73559319c27ab","repo":"linera-io/linera-protocol","slug":"expected-a-tip-hash-string-but-got-invalid-data","errorCode":null,"errorMessage":"Expected a tip hash string, but got {invalid_data:?} instead","messagePattern":"Expected a tip hash string, but got (.+?) instead","errorType":"exception","errorClass":"anyhow","httpStatus":null,"severity":"error","filePath":"linera-service/src/cli_wrappers/wallet.rs","lineNumber":2033,"sourceCode":"            r#\"query {{ block(chainId: \"{chain}\") {{\n                hash\n                block {{ header {{ height }} }}\n            }} }}\"#\n        );\n\n        let mut response = self.query_node(&query).await?;\n\n        match (\n            mem::take(&mut response[\"block\"][\"hash\"]),\n            mem::take(&mut response[\"block\"][\"block\"][\"header\"][\"height\"]),\n        ) {\n            (Value::Null, Value::Null) => Ok(None),\n            (Value::String(hash), Value::Number(height)) => Ok(Some((\n                hash.parse()\n                    .context(\"Received an invalid hash {hash:?} for chain tip\")?,\n                BlockHeight(height.as_u64().unwrap()),\n            ))),\n            invalid_data => bail!(\"Expected a tip hash string, but got {invalid_data:?} instead\"),\n        }\n    }\n\n    /// Subscribes to the node service and returns a stream of notifications about a chain.\n    pub async fn notifications(\n        &self,\n        chain_id: ChainId,\n    ) -> Result<Pin<Box<impl Stream<Item = Result<Notification>>>>> {\n        let query = format!(\"subscription {{ notifications(chainId: \\\"{chain_id}\\\") }}\",);\n        let url = format!(\"ws://localhost:{}/ws\", self.port);\n        let mut request = url.into_client_request()?;\n        request.headers_mut().insert(\n            \"Sec-WebSocket-Protocol\",\n            HeaderValue::from_str(\"graphql-transport-ws\")?,\n        );\n        let (mut websocket, _) = async_tungstenite::tokio::connect_async(request).await?;\n        let init_json = json!({\n          \"type\": \"connection_init\",","sourceCodeStart":2015,"sourceCodeEnd":2051,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-service/src/cli_wrappers/wallet.rs#L2015-L2051","documentation":"`chain_tip` parses the response of `query { block(chainId: ...) { hash block { header { height } } } }`. It expects one of exactly two shapes: both `hash` and `height` null (unknown chain → `None`), or `hash` a JSON string and `height` a JSON number. Any other combination — one side null, hash as a number, height as a string — falls into the `invalid_data` arm and bails with the offending `(hash, height)` pair.","triggerScenarios":"A node-service response where `block.hash`/`block.block.header.height` have unexpected JSON types or only one of the two is null — most often a version/schema mismatch between the wallet wrapper and the running service.","commonSituations":"Wrapper crate built from a different commit than the running `linera service`; a chain in a transient state where one field is populated but not the other; service responses shaped differently after a schema change.","solutions":["Print the raw GraphQL response to see the actual `(hash, height)` pair the wrapper choked on","Rebuild/reinstall wrapper and node service from the same source version","If the chain is expected unknown, confirm the service really returns null for both fields","In throwaway scripts, parse defensively: treat any mismatched pair as 'no tip' instead of failing"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"use serde_json::Value;\n\n/// Returns Some(result) only for shapes chain_tip can parse.\nfn as_chain_tip(hash: &Value, height: &Value) -> Option<Option<(CryptoHash, BlockHeight)>> {\n    match (hash, height) {\n        (Value::Null, Value::Null) => Some(None),\n        (Value::String(h), Value::Number(n)) => {\n            let height = n.as_u64()?;\n            let hash = h.parse().ok()?;\n            Some(Some((hash, BlockHeight(height))))\n        }\n        _ => None, // mismatched pair: reject before chain_tip bails\n    }\n}","tryCatchPattern":"match client.chain_tip(chain).await {\n    Err(e) if e.to_string().contains(\"Expected a tip hash string\") => {\n        // schema/type mismatch with the service; dump the raw response and\n        // check wrapper-vs-service version alignment before retrying\n        dump_raw_block_response(chain).await;\n        Err(e)\n    }\n    result => result,\n}","preventionTips":["Build the cli_wrappers crate and `linera service` from the same commit — schema drift is the usual cause","In tests that tolerate unknown chains, handle both the Ok(None) case and the parse error distinctly","When debugging, reproduce the query (`query { block(chainId: ...) { hash block { header { height } } } }`) against the service to inspect the raw JSON"],"tags":["graphql","parsing","node-service","cli-wrappers"],"backgroundTag":"response-schema-mismatch","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}