From 73ae2a68981eee432250535e586fb3bd99fbefdf Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Fri, 12 Dec 2025 20:42:02 -0600 Subject: [PATCH 1/3] fix(fdev): wait for server response before closing WebSocket connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fdev tool was sending Put requests but immediately closing the WebSocket connection without waiting for the server's response. This caused "Connection reset without closing handshake" errors on the server side and the contract was never stored. The fix modifies execute_command in commands/v1.rs to call recv() after send(), waiting for the server to acknowledge the operation before dropping the connection. Also adds an integration test that verifies the WebSocket client properly waits for responses. Fixes #2278 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/fdev/src/commands/v1.rs | 42 +++++++- crates/fdev/tests/websocket_response.rs | 132 ++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 crates/fdev/tests/websocket_response.rs diff --git a/crates/fdev/src/commands/v1.rs b/crates/fdev/src/commands/v1.rs index 01df01ef7..a7940c99a 100644 --- a/crates/fdev/src/commands/v1.rs +++ b/crates/fdev/src/commands/v1.rs @@ -1,3 +1,5 @@ +use freenet_stdlib::client_api::{ContractResponse, HostResponse}; + use super::*; pub(super) async fn start_api_client(cfg: BaseConfig) -> anyhow::Result { @@ -32,5 +34,43 @@ pub(super) async fn execute_command( api_client: &mut WebApi, ) -> anyhow::Result<()> { api_client.send(request).await?; - Ok(()) + + // Wait for the server's response before closing the connection + let response = api_client + .recv() + .await + .map_err(|e| anyhow::anyhow!("Failed to receive response: {e}"))?; + + match response { + HostResponse::ContractResponse(contract_response) => match contract_response { + ContractResponse::PutResponse { key } => { + tracing::info!(%key, "Contract published successfully"); + Ok(()) + } + ContractResponse::UpdateResponse { key, summary } => { + tracing::info!(%key, ?summary, "Contract updated successfully"); + Ok(()) + } + other => { + tracing::warn!(?other, "Unexpected contract response"); + Ok(()) + } + }, + HostResponse::DelegateResponse { key, values } => { + tracing::info!(%key, response_count = values.len(), "Delegate registered successfully"); + Ok(()) + } + HostResponse::Ok => { + tracing::info!("Operation completed successfully"); + Ok(()) + } + HostResponse::QueryResponse(query_response) => { + tracing::info!(?query_response, "Query response received"); + Ok(()) + } + _ => { + tracing::warn!(?response, "Unexpected response type"); + Ok(()) + } + } } diff --git a/crates/fdev/tests/websocket_response.rs b/crates/fdev/tests/websocket_response.rs new file mode 100644 index 000000000..69a6b2854 --- /dev/null +++ b/crates/fdev/tests/websocket_response.rs @@ -0,0 +1,132 @@ +//! Integration test verifying fdev properly waits for server responses +//! +//! This test ensures that the WebSocket client doesn't close the connection +//! before receiving a response from the server (fixing issue #2278). + +use freenet_stdlib::client_api::{ + ClientRequest, ContractRequest, ContractResponse, HostResponse, WebApi, +}; +use freenet_stdlib::prelude::*; +use std::net::Ipv4Addr; +use std::sync::atomic::{AtomicU16, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::TcpListener; +use tokio::sync::oneshot; +use tokio_tungstenite::tungstenite::Message; + +static PORT: AtomicU16 = AtomicU16::new(54000); + +/// Test that the WebSocket client properly waits for a PutResponse +/// +/// Before the fix for #2278, fdev would close the connection before +/// receiving the response, causing "Connection reset without closing handshake" +/// errors on the server side. +#[tokio::test] +async fn test_websocket_client_waits_for_put_response() { + let port = PORT.fetch_add(1, Ordering::SeqCst); + + // Create a mock contract key for the response (base58 encoded) + let mock_key = ContractKey::from_id("11111111111111111111111111111111").expect("valid key"); + let response: HostResponse = + HostResponse::ContractResponse(ContractResponse::PutResponse { key: mock_key }); + + // Channel to signal when server received request + let (request_tx, request_rx) = oneshot::channel::(); + + // Start the mock server + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, port)) + .await + .expect("bind"); + + let server_response = response.clone(); + let server_handle = tokio::spawn(async move { + let (stream, _) = tokio::time::timeout(Duration::from_secs(5), listener.accept()) + .await + .expect("accept timeout") + .expect("accept"); + + let mut ws_stream = tokio_tungstenite::accept_async(stream) + .await + .expect("ws accept"); + + use futures::{SinkExt, StreamExt}; + + // Receive the request + let msg = tokio::time::timeout(Duration::from_secs(5), ws_stream.next()) + .await + .expect("receive timeout") + .expect("stream not empty") + .expect("receive"); + + // Just verify we received a binary message (which is what contract requests are) + match msg { + Message::Binary(_) => {} // Request received successfully + _ => panic!("expected binary message"), + }; + + // Signal that we received the request + let _ = request_tx.send(true); + + // Send back the response + let response_bytes = bincode::serialize(&Ok::<_, freenet_stdlib::client_api::ClientError>( + server_response, + )) + .expect("serialize"); + ws_stream + .send(Message::Binary(response_bytes.into())) + .await + .expect("send response"); + + // Give the client time to receive the response + tokio::time::sleep(Duration::from_millis(100)).await; + }); + + // Give server time to start listening + tokio::time::sleep(Duration::from_millis(50)).await; + + // Connect client + let url = format!("ws://127.0.0.1:{port}/v1/contract/command?encodingProtocol=native"); + let (stream, _) = tokio_tungstenite::connect_async(&url) + .await + .expect("connect"); + let mut client = WebApi::start(stream); + + // Create a minimal contract for the request + let code = ContractCode::from(vec![0u8; 32]); + let wrapped = WrappedContract::new(Arc::new(code), Parameters::from(vec![])); + let api_version = ContractWasmAPIVersion::V1(wrapped); + let contract = ContractContainer::from(api_version); + + // Send a Put request (simulating what fdev does) + let request = ClientRequest::ContractOp(ContractRequest::Put { + contract, + state: WrappedState::new(vec![]), + related_contracts: RelatedContracts::default(), + subscribe: false, + }); + + client.send(request).await.expect("send request"); + + // This is the key fix: we must receive the response before dropping the client + // Before the fix, fdev would exit here without waiting, causing connection reset + let response = tokio::time::timeout(Duration::from_secs(5), client.recv()) + .await + .expect("response timeout") + .expect("receive response"); + + // Verify we got the expected response + match response { + HostResponse::ContractResponse(ContractResponse::PutResponse { key }) => { + assert_eq!(key, mock_key); + } + other => panic!("unexpected response: {:?}", other), + } + + // Verify the server received the request + let received = request_rx.await.expect("server signaled"); + assert!(received, "server should have received the request"); + + // Wait for server to complete + server_handle.await.expect("server task"); +} From d39f4e6b9e29199991b616ea836ab72d6aef8abc Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Fri, 12 Dec 2025 21:43:33 -0600 Subject: [PATCH 2/3] fix(fdev): wait for server response at callsites, not in execute_command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: keep execute_command() as send-only to maintain API consistency with query.rs and diagnostics.rs which call recv() explicitly after execute_command(). Changes: - Revert execute_command() to just send (no recv) - Add explicit recv() calls at put_contract, put_delegate, and update callsites with proper response type validation - Return errors for unexpected responses instead of silently succeeding - Remove fragile sleep-based timing in test, use channel synchronization This approach: - Maintains consistent API behavior across all operations - Forces proper error handling at each callsite - Follows the existing pattern in query.rs and diagnostics.rs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/fdev/src/commands.rs | 74 ++++++++++++++++++++++--- crates/fdev/src/commands/v1.rs | 42 +------------- crates/fdev/tests/websocket_response.rs | 27 ++++----- 3 files changed, 77 insertions(+), 66 deletions(-) diff --git a/crates/fdev/src/commands.rs b/crates/fdev/src/commands.rs index cc7670edd..edaac48d8 100644 --- a/crates/fdev/src/commands.rs +++ b/crates/fdev/src/commands.rs @@ -5,7 +5,9 @@ use freenet_stdlib::prelude::{ ContractCode, ContractContainer, ContractWasmAPIVersion, Parameters, WrappedContract, }; use freenet_stdlib::{ - client_api::{ClientRequest, ContractRequest, DelegateRequest, WebApi}, + client_api::{ + ClientRequest, ContractRequest, ContractResponse, DelegateRequest, HostResponse, WebApi, + }, prelude::*, }; use xz2::read::XzDecoder; @@ -170,9 +172,26 @@ async fn put_contract( tracing::debug!("Starting WebSocket client connection"); let mut client = start_api_client(other).await?; tracing::debug!("WebSocket client connected successfully"); - let result = execute_command(request, &mut client).await; - tracing::debug!(success = ?result.is_ok(), "WebSocket client operation complete"); - result + execute_command(request, &mut client).await?; + + // Wait for server response before closing connection + let response = client + .recv() + .await + .map_err(|e| anyhow::anyhow!("Failed to receive response: {e}"))?; + + match response { + HostResponse::ContractResponse(ContractResponse::PutResponse { key: response_key }) => { + tracing::info!(%response_key, "Contract published successfully"); + Ok(()) + } + HostResponse::ContractResponse(other) => { + anyhow::bail!("Unexpected contract response: {:?}", other) + } + other => { + anyhow::bail!("Unexpected response type: {:?}", other) + } + } } async fn put_delegate( @@ -204,7 +223,8 @@ For additional hardening is recommended to use a different cipher and nonce to e (cipher, nonce) }; - println!("Putting delegate {} ", delegate.key().encode()); + let delegate_key = delegate.key().clone(); + println!("Putting delegate {} ", delegate_key.encode()); let request = DelegateRequest::RegisterDelegate { delegate, cipher, @@ -212,7 +232,23 @@ For additional hardening is recommended to use a different cipher and nonce to e } .into(); let mut client = start_api_client(other).await?; - execute_command(request, &mut client).await + execute_command(request, &mut client).await?; + + // Wait for server response before closing connection + let response = client + .recv() + .await + .map_err(|e| anyhow::anyhow!("Failed to receive response: {e}"))?; + + match response { + HostResponse::DelegateResponse { key, values } => { + tracing::info!(%key, response_count = values.len(), "Delegate registered successfully"); + Ok(()) + } + other => { + anyhow::bail!("Unexpected response type: {:?}", other) + } + } } #[derive(clap::Parser, Clone, Debug)] @@ -253,7 +289,7 @@ pub async fn update(config: UpdateConfig, other: BaseConfig) -> anyhow::Result<( if config.release { anyhow::bail!("Cannot publish contracts in the network yet"); } - let key = ContractInstanceId::try_from(config.key)?.into(); + let key: ContractKey = ContractInstanceId::try_from(config.key)?.into(); println!("Updating contract {key}"); let data = { let mut buf = vec![]; @@ -262,7 +298,29 @@ pub async fn update(config: UpdateConfig, other: BaseConfig) -> anyhow::Result<( }; let request = ContractRequest::Update { key, data }.into(); let mut client = start_api_client(other).await?; - execute_command(request, &mut client).await + execute_command(request, &mut client).await?; + + // Wait for server response before closing connection + let response = client + .recv() + .await + .map_err(|e| anyhow::anyhow!("Failed to receive response: {e}"))?; + + match response { + HostResponse::ContractResponse(ContractResponse::UpdateResponse { + key: response_key, + summary, + }) => { + tracing::info!(%response_key, ?summary, "Contract updated successfully"); + Ok(()) + } + HostResponse::ContractResponse(other) => { + anyhow::bail!("Unexpected contract response: {:?}", other) + } + other => { + anyhow::bail!("Unexpected response type: {:?}", other) + } + } } pub(crate) async fn start_api_client(cfg: BaseConfig) -> anyhow::Result { diff --git a/crates/fdev/src/commands/v1.rs b/crates/fdev/src/commands/v1.rs index a7940c99a..01df01ef7 100644 --- a/crates/fdev/src/commands/v1.rs +++ b/crates/fdev/src/commands/v1.rs @@ -1,5 +1,3 @@ -use freenet_stdlib::client_api::{ContractResponse, HostResponse}; - use super::*; pub(super) async fn start_api_client(cfg: BaseConfig) -> anyhow::Result { @@ -34,43 +32,5 @@ pub(super) async fn execute_command( api_client: &mut WebApi, ) -> anyhow::Result<()> { api_client.send(request).await?; - - // Wait for the server's response before closing the connection - let response = api_client - .recv() - .await - .map_err(|e| anyhow::anyhow!("Failed to receive response: {e}"))?; - - match response { - HostResponse::ContractResponse(contract_response) => match contract_response { - ContractResponse::PutResponse { key } => { - tracing::info!(%key, "Contract published successfully"); - Ok(()) - } - ContractResponse::UpdateResponse { key, summary } => { - tracing::info!(%key, ?summary, "Contract updated successfully"); - Ok(()) - } - other => { - tracing::warn!(?other, "Unexpected contract response"); - Ok(()) - } - }, - HostResponse::DelegateResponse { key, values } => { - tracing::info!(%key, response_count = values.len(), "Delegate registered successfully"); - Ok(()) - } - HostResponse::Ok => { - tracing::info!("Operation completed successfully"); - Ok(()) - } - HostResponse::QueryResponse(query_response) => { - tracing::info!(?query_response, "Query response received"); - Ok(()) - } - _ => { - tracing::warn!(?response, "Unexpected response type"); - Ok(()) - } - } + Ok(()) } diff --git a/crates/fdev/tests/websocket_response.rs b/crates/fdev/tests/websocket_response.rs index 69a6b2854..a8e236f16 100644 --- a/crates/fdev/tests/websocket_response.rs +++ b/crates/fdev/tests/websocket_response.rs @@ -31,8 +31,8 @@ async fn test_websocket_client_waits_for_put_response() { let response: HostResponse = HostResponse::ContractResponse(ContractResponse::PutResponse { key: mock_key }); - // Channel to signal when server received request - let (request_tx, request_rx) = oneshot::channel::(); + // Channel to signal when server is ready to accept connections + let (ready_tx, ready_rx) = oneshot::channel::<()>(); // Start the mock server let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, port)) @@ -41,6 +41,9 @@ async fn test_websocket_client_waits_for_put_response() { let server_response = response.clone(); let server_handle = tokio::spawn(async move { + // Signal that we're ready to accept connections + let _ = ready_tx.send(()); + let (stream, _) = tokio::time::timeout(Duration::from_secs(5), listener.accept()) .await .expect("accept timeout") @@ -59,15 +62,12 @@ async fn test_websocket_client_waits_for_put_response() { .expect("stream not empty") .expect("receive"); - // Just verify we received a binary message (which is what contract requests are) + // Verify we received a binary message (contract requests are binary) match msg { Message::Binary(_) => {} // Request received successfully _ => panic!("expected binary message"), }; - // Signal that we received the request - let _ = request_tx.send(true); - // Send back the response let response_bytes = bincode::serialize(&Ok::<_, freenet_stdlib::client_api::ClientError>( server_response, @@ -77,13 +77,10 @@ async fn test_websocket_client_waits_for_put_response() { .send(Message::Binary(response_bytes.into())) .await .expect("send response"); - - // Give the client time to receive the response - tokio::time::sleep(Duration::from_millis(100)).await; }); - // Give server time to start listening - tokio::time::sleep(Duration::from_millis(50)).await; + // Wait for server to be ready before connecting + ready_rx.await.expect("server ready signal"); // Connect client let url = format!("ws://127.0.0.1:{port}/v1/contract/command?encodingProtocol=native"); @@ -108,8 +105,8 @@ async fn test_websocket_client_waits_for_put_response() { client.send(request).await.expect("send request"); - // This is the key fix: we must receive the response before dropping the client - // Before the fix, fdev would exit here without waiting, causing connection reset + // This is the key behavior: we must receive the response before dropping the client. + // Before the fix, fdev would exit here without waiting, causing connection reset. let response = tokio::time::timeout(Duration::from_secs(5), client.recv()) .await .expect("response timeout") @@ -123,10 +120,6 @@ async fn test_websocket_client_waits_for_put_response() { other => panic!("unexpected response: {:?}", other), } - // Verify the server received the request - let received = request_rx.await.expect("server signaled"); - assert!(received, "server should have received the request"); - // Wait for server to complete server_handle.await.expect("server task"); } From 066e6eb95cc0063e1c9236b501362de1cfd6e91f Mon Sep 17 00:00:00 2001 From: Ian Clarke Date: Sat, 13 Dec 2025 17:51:17 -0600 Subject: [PATCH 3/3] fix: server completes PUT operations even when client disconnects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the server-side root cause of issue #2278. Previously, when a client disconnected while a PUT operation was in progress, the `send_to_sender()` call would fail and propagate the error via `?`, causing the entire contract handler loop to exit. This broke all future contract operations for the node. The fix changes all `send_to_sender()` calls in the contract handler to use non-propagating error handling: if the response can't be delivered (e.g., because the client disconnected), we log at debug level and continue processing other events. The actual work (storing the contract, applying updates, etc.) has already been completed before the response is sent, so failing to deliver the response is not fatal. Changes: - contract/mod.rs: Change all `send_to_sender()?` to `if let Err(e) = send_to_sender() { log }` - contract/handler.rs: Add regression test verifying send_to_sender fails gracefully The client-side fix from the previous commit ensures fdev waits for the response, so the response delivery usually succeeds. This server-side fix ensures the node stays healthy even in edge cases where the client disconnects unexpectedly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/core/src/contract/handler.rs | 79 +++++++++++++++++++ crates/core/src/contract/mod.rs | 116 ++++++++++++++++------------ 2 files changed, 146 insertions(+), 49 deletions(-) diff --git a/crates/core/src/contract/handler.rs b/crates/core/src/contract/handler.rs index c8bbe49fe..9f76aea93 100644 --- a/crates/core/src/contract/handler.rs +++ b/crates/core/src/contract/handler.rs @@ -664,6 +664,85 @@ pub mod test { Ok(()) } + + /// Regression test for issue #2278: Verifies that send_to_sender returns + /// an error when the response receiver is dropped, but does NOT crash or + /// break the channel. + /// + /// This tests that the channel infrastructure supports the fix in + /// contract/mod.rs where we changed `send_to_sender()?` to non-propagating + /// error handling, so the handler loop can continue even when a response + /// can't be delivered. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn send_to_sender_fails_gracefully_when_receiver_dropped() -> anyhow::Result<()> { + let (send_halve, mut rcv_halve, _) = contract_handler_channel(); + + let contract = ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new( + Arc::new(ContractCode::from(vec![0, 1, 2, 3])), + Parameters::from(vec![4, 5]), + ))); + let key = contract.key(); + + // Send a request + let h = GlobalExecutor::spawn({ + async move { + send_halve + .send_to_handler(ContractHandlerEvent::PutQuery { + key, + state: vec![6, 7, 8].into(), + related_contracts: RelatedContracts::default(), + contract: None, + }) + .await + } + }); + + // Receive the event from the handler side + let (id, ev) = + tokio::time::timeout(Duration::from_millis(100), rcv_halve.recv_from_sender()) + .await??; + + // Verify it's a PutQuery + let ContractHandlerEvent::PutQuery { state, .. } = ev else { + anyhow::bail!("expected PutQuery event"); + }; + assert_eq!(state.as_ref(), &[6, 7, 8]); + + // Abort the request handler task - this drops the oneshot receiver + // simulating a client disconnect + h.abort(); + // Wait a bit for the abort to take effect + tokio::time::sleep(Duration::from_millis(10)).await; + + // Try to send the response - this should fail because the receiver was dropped + // when we aborted the task. In the actual contract_handling loop (after the fix), + // this would just log and continue rather than propagating the error. + let send_result = rcv_halve + .send_to_sender( + id, + ContractHandlerEvent::PutResponse { + new_value: Ok(vec![0, 7].into()), + }, + ) + .await; + + // send_to_sender should fail because receiver was dropped + assert!( + send_result.is_err(), + "send_to_sender should fail when receiver is dropped, got {:?}", + send_result + ); + + // Verify the error is the expected NoEvHandlerResponse + // Use super:: to disambiguate from freenet_stdlib::prelude::ContractError + assert!( + matches!(send_result, Err(super::ContractError::NoEvHandlerResponse)), + "Expected NoEvHandlerResponse error, got {:?}", + send_result + ); + + Ok(()) + } } pub(super) mod in_memory { diff --git a/crates/core/src/contract/mod.rs b/crates/core/src/contract/mod.rs index 7be7c8eff..3fbb6cfef 100644 --- a/crates/core/src/contract/mod.rs +++ b/crates/core/src/contract/mod.rs @@ -55,7 +55,8 @@ where phase = "get_complete", "Fetched contract" ); - contract_handler + // Send response back to caller. If the caller disconnected, just log and continue. + if let Err(error) = contract_handler .channel() .send_to_sender( id, @@ -65,13 +66,13 @@ where }, ) .await - .map_err(|error| { - tracing::debug!( - error = %error, - "Shutting down contract handler" - ); - error - })?; + { + tracing::debug!( + error = %error, + contract = %key, + "Failed to send GET response (client may have disconnected)" + ); + } } Err(err) => { tracing::warn!( @@ -89,7 +90,8 @@ where ); return Err(ContractError::FatalExecutorError { key, error: err }); } - contract_handler + // Send error response back to caller. If the caller disconnected, just log and continue. + if let Err(error) = contract_handler .channel() .send_to_sender( id, @@ -99,13 +101,13 @@ where }, ) .await - .map_err(|error| { - tracing::debug!( - error = %error, - "Shutting down contract handler" - ); - error - })?; + { + tracing::debug!( + error = %error, + contract = %key, + "Failed to send GET error response (client may have disconnected)" + ); + } } } } @@ -168,17 +170,20 @@ where } }; - contract_handler + // Send response back to caller. If the caller disconnected (e.g., WebSocket closed), + // the response channel may be dropped. This is not fatal - the contract has already + // been stored, so we just log and continue processing other events. + if let Err(error) = contract_handler .channel() .send_to_sender(id, event_result) .await - .map_err(|error| { - tracing::debug!( - error = %error, - "Shutting down contract handler" - ); - error - })?; + { + tracing::debug!( + error = %error, + contract = %key, + "Failed to send PUT response (client may have disconnected)" + ); + } } ContractHandlerEvent::UpdateQuery { key, @@ -258,17 +263,19 @@ where } }; - contract_handler + // Send response back to caller. If the caller disconnected, the response channel + // may be dropped. This is not fatal - the update has already been applied. + if let Err(error) = contract_handler .channel() .send_to_sender(id, event_result) .await - .map_err(|error| { - tracing::debug!( - error = %error, - "Shutting down contract handler" - ); - error - })?; + { + tracing::debug!( + error = %error, + contract = %key, + "Failed to send UPDATE response (client may have disconnected)" + ); + } } ContractHandlerEvent::DelegateRequest { req, @@ -309,17 +316,19 @@ where } }; - contract_handler + // Send response back to caller. If the caller disconnected, the response channel + // may be dropped. This is not fatal - the delegate has already been processed. + if let Err(error) = contract_handler .channel() .send_to_sender(id, ContractHandlerEvent::DelegateResponse(response)) .await - .map_err(|error| { - tracing::debug!( - error = %error, - "Shutting down contract handler" - ); - error - })?; + { + tracing::debug!( + error = %error, + delegate_key = %delegate_key, + "Failed to send DELEGATE response (client may have disconnected)" + ); + } } ContractHandlerEvent::RegisterSubscriberListener { key, @@ -340,14 +349,19 @@ where ); }); - // FIXME: if there is an error senc actually an error back - contract_handler + // FIXME: if there is an error send actually an error back + // If the caller disconnected, just log and continue. + if let Err(error) = contract_handler .channel() .send_to_sender(id, ContractHandlerEvent::RegisterSubscriberListenerResponse) .await - .inspect_err(|error| { - tracing::debug!(%error, "shutting down contract handler"); - })?; + { + tracing::debug!( + error = %error, + contract = %key, + "Failed to send RegisterSubscriberListener response (client may have disconnected)" + ); + } } ContractHandlerEvent::QuerySubscriptions { callback } => { // Get subscription information from the executor and send it through the callback @@ -362,13 +376,17 @@ where .send(crate::message::QueryResult::NetworkDebug(network_debug)) .await; - contract_handler + // If the caller disconnected, just log and continue. + if let Err(error) = contract_handler .channel() .send_to_sender(id, ContractHandlerEvent::QuerySubscriptionsResponse) .await - .inspect_err(|error| { - tracing::debug!(%error, "shutting down contract handler"); - })?; + { + tracing::debug!( + error = %error, + "Failed to send QuerySubscriptions response (client may have disconnected)" + ); + } } _ => unreachable!("ContractHandlerEvent enum should be exhaustive here"), }