-
- Notifications
You must be signed in to change notification settings - Fork 111
fix(fdev): wait for server response before closing WebSocket connection #2280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
73ae2a6 d39f4e6 066e6eb a348c95 d6040b1 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| use freenet_stdlib::client_api::{ContractResponse, HostResponse}; | ||
| | ||
| use super::*; | ||
| | ||
| pub(super) async fn start_api_client(cfg: BaseConfig) -> anyhow::Result<WebApi> { | ||
| | @@ -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(()) | ||
| } | ||
| } | ||
| ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<WrappedState> = | ||
| HostResponse::ContractResponse(ContractResponse::PutResponse { key: mock_key }); | ||
| | ||
| // Channel to signal when server received request | ||
| let (request_tx, request_rx) = oneshot::channel::<bool>(); | ||
| | ||
| // 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"); | ||
| } | ||
| Comment on lines 26 to 125 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The catch-all pattern at line 71 using
_will match error responses likeHostResponse::Errwithout proper handling. Error responses from the server should be treated as failures rather than logged as warnings and returningOk(()). Consider explicitly matchingHostResponse::Errand returning an error in that case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid point. In the restructured approach, error handling will be at each callsite where we know the expected response type, making it clearer what constitutes an error.
[AI-assisted - Claude]