{"record":{"id":"81ec63b5c0bc22ae","repo":"FuelLabs/fuel-core","slug":"stream-closed-without-transaction-status","errorCode":null,"errorMessage":"Stream closed without transaction status","messagePattern":"Stream closed without transaction status","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/fuel-core/src/service/query.rs","lineNumber":76,"sourceCode":"        &self,\n        tx: Transaction,\n    ) -> anyhow::Result<TransactionStatus> {\n        let id = tx.id(&self\n            .shared\n            .config\n            .snapshot_reader\n            .chain_config()\n            .consensus_parameters\n            .chain_id());\n        let stream = self.transaction_status_change(id).await?.filter(|status| {\n            futures::future::ready(status.as_ref().is_ok_and(|status| status.is_final()))\n        });\n        futures::pin_mut!(stream);\n        self.submit(tx).await?;\n        stream\n            .next()\n            .await\n            .ok_or_else(|| anyhow::anyhow!(\"Stream closed without transaction status\"))?\n    }\n\n    /// Return a stream of status changes for a transaction.\n    pub async fn transaction_status_change(\n        &self,\n        id: Bytes32,\n    ) -> anyhow::Result<impl Stream<Item = anyhow::Result<TransactionStatus>> + '_> {\n        // First subscribe to the statuses, and only after that create a view.\n        let tx_status_manager = &self.shared.tx_status_manager;\n        let rx = tx_status_manager.tx_update_subscribe(id).await?;\n        let db = self.shared.database.off_chain().latest_view()?;\n        let state = StatusChangeState {\n            db,\n            tx_status_manager,\n        };\n        Ok(transaction_status_change(state, rx, id, true).await)\n    }\n}","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/fuel-core/src/service/query.rs#L58-L94","documentation":"Thrown by QueryService::submit_and_await_commit (crates/fuel-core/src/service/query.rs:57). The node subscribes to status updates for the transaction id, submits the tx, then awaits the next final status on the filtered stream. The stream returning None means the status-update channel closed before any final status was delivered — in practice the tx status manager or the node itself was dropped or shut down mid-wait.","triggerScenarios":"The node shuts down (SharedState or tx_status_manager dropped) while the caller is awaiting commit; a restart lands between submission and final status; abnormal service teardown during heavy load.","commonSituations":"Automation that stops the node right after submitting a transaction; container or pod restarts mid-await; clients holding the future across process boundaries; changes in status manager lifecycle across fuel-core versions.","solutions":["Check node liveness and logs first — a shutdown or panic during the await is the usual cause; restart the node.","Retry the submission after restart: submitting the same signed transaction is idempotent (same tx id), then await the status again.","Use transaction_status_change (the raw stream API) and handle closure explicitly when you need custom retry logic instead of submit_and_await_commit.","Wrap the await in a client-side timeout so node shutdowns surface as timeouts rather than dangling awaits."],"exampleFix":"// before: single shot, fails when the node restarts mid-await\nlet status = client.submit_and_await_commit(tx).await?;\n\n// after: retry on stream closure (same signed tx -> same id, safe to resubmit)\nloop {\n    match client.submit_and_await_commit(tx.clone()).await {\n        Ok(status) => break status,\n        Err(e) if e.to_string().contains(\"Stream closed\") => {\n            tokio::time::sleep(Duration::from_secs(1)).await;\n            continue;\n        }\n        Err(e) => return Err(e),\n    }\n}","handlingStrategy":"retry","validationCode":"// Pre-flight liveness check before submitting\nasync fn node_alive(client: &fuel_client::client::FuelClient) -> bool {\n    client.health().await.is_ok()\n}\n\nif !node_alive(&client).await {\n    return Err(anyhow::anyhow!(\"node unreachable before submit\"));\n}","typeGuard":null,"tryCatchPattern":"loop {\n    match client.submit_and_await_commit(tx.clone()).await {\n        Ok(status) => break Ok(status),\n        Err(e) if e.to_string().contains(\"Stream closed without transaction status\") => {\n            // node likely restarted; wait, verify health, then resubmit the same signed tx\n            tokio::time::sleep(std::time::Duration::from_secs(1)).await;\n            continue;\n        }\n        Err(e) => break Err(e),\n    }\n}","preventionTips":["Do not stop or restart the node between submit and final status when using submit_and_await_commit.","Wrap the await in a client-side timeout so shutdowns surface deterministically.","Prefer the transaction_status_change stream when you need explicit control over channel closure.","Resubmitting the same signed transaction is safe: it deduplicates by tx id."],"tags":["async","transaction-status","lifecycle","fuel-core","rust"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}