1use dora_message::ws_protocol::WsRequest;
7use eyre::{Context, eyre};
8use futures::{SinkExt, StreamExt};
9use std::{collections::HashMap, net::SocketAddr, sync::mpsc as std_mpsc};
10use tokio::sync::{mpsc, oneshot};
11use tokio_tungstenite::tungstenite::Message;
12use uuid::Uuid;
13
14#[derive(serde::Deserialize)]
19struct IncomingFrame {
20 #[serde(default)]
21 id: Option<Uuid>,
22 #[serde(default)]
23 event: Option<String>,
24 #[serde(default)]
25 result: Option<Box<serde_json::value::RawValue>>,
26 #[serde(default)]
27 error: Option<String>,
28 #[serde(default)]
29 payload: Option<Box<serde_json::value::RawValue>>,
30}
31
32pub struct WsSession {
37 rt: tokio::runtime::Runtime,
38 cmd_tx: mpsc::UnboundedSender<SessionCommand>,
39}
40
41enum SessionCommand {
42 Request {
44 data: Vec<u8>,
45 reply: oneshot::Sender<eyre::Result<Vec<u8>>>,
46 },
47 SubscribeLogs {
49 request: Vec<u8>,
50 log_tx: std_mpsc::Sender<eyre::Result<Vec<u8>>>,
51 ack_tx: oneshot::Sender<eyre::Result<()>>,
52 },
53 SubscribeTopics {
55 request: Vec<u8>,
56 data_tx: std_mpsc::Sender<eyre::Result<Vec<u8>>>,
57 ack_tx: oneshot::Sender<eyre::Result<Uuid>>,
58 },
59}
60
61impl WsSession {
62 pub fn connect(addr: SocketAddr) -> eyre::Result<Self> {
68 if tokio::runtime::Handle::try_current().is_ok() {
69 eyre::bail!(
70 "WsSession::connect must not be called from within an async context; \
71 use an async-native client instead"
72 );
73 }
74 let rt = tokio::runtime::Builder::new_multi_thread()
75 .worker_threads(1)
76 .enable_all()
77 .build()
78 .context("failed to create tokio runtime for WS session")?;
79
80 let ws_url = format!("ws://{addr}/api/control");
81 let ws_stream = rt
82 .block_on(async {
83 use tokio_tungstenite::tungstenite;
84 let mut request = tungstenite::http::Request::builder()
85 .uri(&ws_url)
86 .header("Host", addr.to_string())
87 .header("Connection", "Upgrade")
88 .header("Upgrade", "websocket")
89 .header(
90 "Sec-WebSocket-Key",
91 tungstenite::handshake::client::generate_key(),
92 )
93 .header("Sec-WebSocket-Version", "13");
94 if let Some(token) = dora_message::auth::discover_token() {
95 request = request.header("Authorization", format!("Bearer {}", token.as_hex()));
96 }
97 let request = request.body(()).expect("failed to build WS request");
98 tokio_tungstenite::connect_async(request).await
99 })
100 .map_err(|e| {
101 let msg = e.to_string();
102 if msg.to_lowercase().contains("connection refused")
103 || msg.contains("No connection could be made")
104 {
105 eyre!(
106 "cannot connect to coordinator at {addr}: {msg}\n\n \
107 hint: is the coordinator running? Start it with `dora up`"
108 )
109 } else if msg.contains("401") || msg.contains("Unauthorized") {
110 eyre!(
111 "authentication failed connecting to coordinator at {addr}: {msg}\n\n \
112 The coordinator was started with --auth and requires a valid token.\n \
113 The token is stored in ~/.config/dora/.dora-token\n\n \
114 Possible fixes:\n \
115 - Run `dora down && dora up` to regenerate the token\n \
116 - Ensure you're using the same user that started the coordinator\n \
117 - Set DORA_AUTH_TOKEN env var to match the coordinator's token\n \
118 - Restart without --auth to disable authentication"
119 )
120 } else {
121 eyre!("failed to connect to coordinator at {addr}: {msg}")
122 }
123 })?
124 .0;
125
126 let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
127 rt.spawn(session_loop(ws_stream, cmd_rx));
128
129 let session = Self { rt, cmd_tx };
130
131 session.handshake_hello()?;
136
137 Ok(session)
138 }
139
140 fn handshake_hello(&self) -> eyre::Result<()> {
144 use dora_message::{
145 cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply,
146 };
147 let req = serde_json::to_vec(&ControlRequest::hello())
148 .map_err(|e| eyre!("failed to serialize Hello: {e}"))?;
149 let raw_reply = self.request(&req).wrap_err(
150 "protocol version handshake with coordinator failed \
151 (could not send or receive Hello)",
152 )?;
153 let reply: ControlRequestReply = serde_json::from_slice(&raw_reply)
154 .map_err(|e| eyre!("failed to parse Hello reply: {e}"))?;
155 match reply {
156 ControlRequestReply::HelloOk { dora_version } => {
157 tracing::debug!(
158 coordinator_version = %dora_version,
159 "protocol version handshake OK"
160 );
161 Ok(())
162 }
163 ControlRequestReply::Error(msg) => Err(eyre!(
164 "coordinator rejected CLI: {msg}\n\n \
165 hint: the CLI and coordinator binaries must share a \
166 semver-compatible dora version. Upgrade the component \
167 that is behind."
168 )),
169 other => Err(eyre!(
170 "unexpected reply to Hello: {other:?} — \
171 coordinator may be too old to understand the handshake"
172 )),
173 }
174 }
175
176 pub fn request(&self, data: &[u8]) -> eyre::Result<Vec<u8>> {
181 let (reply_tx, reply_rx) = oneshot::channel();
182 self.cmd_tx
183 .send(SessionCommand::Request {
184 data: data.to_vec(),
185 reply: reply_tx,
186 })
187 .map_err(|_| eyre!("WS session closed"))?;
188
189 self.rt
190 .block_on(reply_rx)
191 .map_err(|_| eyre!("WS session dropped reply"))?
192 }
193
194 pub fn subscribe_topics(
200 &self,
201 dataflow_id: Uuid,
202 topics: Vec<(dora_message::id::NodeId, dora_message::id::DataId)>,
203 ) -> eyre::Result<(Uuid, std_mpsc::Receiver<eyre::Result<Vec<u8>>>)> {
204 let request = serde_json::to_vec(
205 &dora_message::cli_to_coordinator::ControlRequest::TopicSubscribe {
206 dataflow_id,
207 topics,
208 protocol_version: Some(dora_message::TOPIC_DATA_PROTOCOL_VERSION),
209 },
210 )
211 .map_err(|e| eyre!("failed to serialize TopicSubscribe: {e}"))?;
212
213 let (data_tx, data_rx) = std_mpsc::channel();
214 let (ack_tx, ack_rx) = oneshot::channel();
215 self.cmd_tx
216 .send(SessionCommand::SubscribeTopics {
217 request,
218 data_tx,
219 ack_tx,
220 })
221 .map_err(|_| eyre!("WS session closed"))?;
222
223 let subscription_id = self
224 .rt
225 .block_on(ack_rx)
226 .map_err(|_| eyre!("WS session dropped ack"))??;
227
228 Ok((subscription_id, data_rx))
229 }
230
231 pub fn subscribe_logs(
238 &self,
239 request: &[u8],
240 ) -> eyre::Result<std_mpsc::Receiver<eyre::Result<Vec<u8>>>> {
241 let (log_tx, log_rx) = std_mpsc::channel();
242 let (ack_tx, ack_rx) = oneshot::channel();
243 self.cmd_tx
244 .send(SessionCommand::SubscribeLogs {
245 request: request.to_vec(),
246 log_tx,
247 ack_tx,
248 })
249 .map_err(|_| eyre!("WS session closed"))?;
250
251 self.rt
252 .block_on(ack_rx)
253 .map_err(|_| eyre!("WS session dropped ack"))??;
254
255 Ok(log_rx)
256 }
257}
258
259type WsStream =
260 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
261type PendingRequests = HashMap<Uuid, oneshot::Sender<eyre::Result<Vec<u8>>>>;
262type PendingSubscribes = HashMap<
263 Uuid,
264 (
265 oneshot::Sender<eyre::Result<()>>,
266 std_mpsc::Sender<eyre::Result<Vec<u8>>>,
267 ),
268>;
269type PendingTopicSubscribes = HashMap<
270 Uuid,
271 (
272 oneshot::Sender<eyre::Result<Uuid>>,
273 std_mpsc::Sender<eyre::Result<Vec<u8>>>,
274 ),
275>;
276type TopicSubscribers = HashMap<Uuid, std_mpsc::Sender<eyre::Result<Vec<u8>>>>;
277
278async fn session_loop(ws_stream: WsStream, mut cmd_rx: mpsc::UnboundedReceiver<SessionCommand>) {
279 let (mut ws_tx, mut ws_rx) = ws_stream.split();
280 let mut pending_requests: PendingRequests = HashMap::new();
281 let mut pending_subscribes: PendingSubscribes = HashMap::new();
282 let mut log_subscribers: Vec<std_mpsc::Sender<eyre::Result<Vec<u8>>>> = Vec::new();
283 let mut pending_topic_subscribes: PendingTopicSubscribes = HashMap::new();
284 let mut topic_subscribers: TopicSubscribers = HashMap::new();
285
286 loop {
287 tokio::select! {
288 Some(cmd) = cmd_rx.recv() => {
289 match cmd {
290 SessionCommand::Request { data, reply } => {
291 let id = Uuid::new_v4();
292 let params = match serde_json::from_slice(&data) {
293 Ok(v) => v,
294 Err(e) => {
295 let _ = reply.send(Err(eyre!("failed to parse request: {e}")));
296 continue;
297 }
298 };
299 let req = WsRequest {
300 id,
301 method: "control".to_string(),
302 params,
303 };
304 let json = match serde_json::to_string(&req) {
305 Ok(j) => j,
306 Err(e) => {
307 let _ = reply.send(Err(eyre!("failed to serialize WsRequest: {e}")));
308 continue;
309 }
310 };
311 pending_requests.insert(id, reply);
312 if ws_tx.send(Message::Text(json.into())).await.is_err() {
313 break;
314 }
315 }
316 SessionCommand::SubscribeLogs { request, log_tx, ack_tx } => {
317 let id = Uuid::new_v4();
318 let params = match serde_json::from_slice(&request) {
319 Ok(v) => v,
320 Err(e) => {
321 let _ = ack_tx.send(Err(eyre!("failed to parse subscribe request: {e}")));
322 continue;
323 }
324 };
325 let req = WsRequest {
326 id,
327 method: "control".to_string(),
328 params,
329 };
330 let json = match serde_json::to_string(&req) {
331 Ok(j) => j,
332 Err(e) => {
333 let _ = ack_tx.send(Err(eyre!("failed to serialize WsRequest: {e}")));
334 continue;
335 }
336 };
337 pending_subscribes.insert(id, (ack_tx, log_tx));
338 if ws_tx.send(Message::Text(json.into())).await.is_err() {
339 break;
340 }
341 }
342 SessionCommand::SubscribeTopics { request, data_tx, ack_tx } => {
343 let id = Uuid::new_v4();
344 let params = match serde_json::from_slice(&request) {
345 Ok(v) => v,
346 Err(e) => {
347 let _ = ack_tx.send(Err(eyre!("failed to parse topic subscribe request: {e}")));
348 continue;
349 }
350 };
351 let req = WsRequest {
352 id,
353 method: "control".to_string(),
354 params,
355 };
356 let json = match serde_json::to_string(&req) {
357 Ok(j) => j,
358 Err(e) => {
359 let _ = ack_tx.send(Err(eyre!("failed to serialize WsRequest: {e}")));
360 continue;
361 }
362 };
363 pending_topic_subscribes.insert(id, (ack_tx, data_tx));
364 if ws_tx.send(Message::Text(json.into())).await.is_err() {
365 break;
366 }
367 }
368 }
369 }
370 msg = ws_rx.next() => {
371 let Some(msg) = msg else { break };
372 match msg {
373 Ok(Message::Text(text)) => {
374 let frame: IncomingFrame = match serde_json::from_str(&text) {
375 Ok(m) => m,
376 Err(e) => {
377 tracing::warn!("failed to parse WS message: {e}");
378 continue;
379 }
380 };
381
382 if let Some(event_name) = &frame.event {
383 if event_name == "log"
384 && let Some(payload) = &frame.payload {
385 let bytes = payload.get().as_bytes().to_vec();
386 log_subscribers.retain(|tx| tx.send(Ok(bytes.clone())).is_ok());
387 }
388 } else if let Some(id) = frame.id {
389 handle_response(
390 id,
391 frame.result,
392 frame.error,
393 &mut pending_requests,
394 &mut pending_subscribes,
395 &mut log_subscribers,
396 &mut pending_topic_subscribes,
397 &mut topic_subscribers,
398 );
399 }
400 }
401 Ok(Message::Binary(data)) => {
402 if data.len() < 16 {
404 tracing::warn!("binary WS frame too short ({} bytes)", data.len());
405 continue;
406 }
407 let Ok(sub_id_bytes): Result<[u8; 16], _> = data[..16].try_into() else {
408 continue;
409 };
410 let sub_id = Uuid::from_bytes(sub_id_bytes);
411 let payload = data[16..].to_vec();
412 if let Some(tx) = topic_subscribers.get(&sub_id)
413 && tx.send(Ok(payload)).is_err() {
414 topic_subscribers.remove(&sub_id);
415 }
416 }
417 Ok(Message::Close(_)) => break,
418 Ok(Message::Ping(data)) => {
419 let _ = ws_tx.send(Message::Pong(data)).await;
420 }
421 Ok(other) => {
422 tracing::trace!("ignoring unexpected WS message type: {other:?}");
423 }
424 Err(_) => break,
425 }
426 }
427 }
428 }
429
430 for (_, reply) in pending_requests.drain() {
432 let _ = reply.send(Err(eyre!("WS connection closed")));
433 }
434 for (_, (ack, _)) in pending_subscribes.drain() {
435 let _ = ack.send(Err(eyre!("WS connection closed")));
436 }
437 for (_, (ack, _)) in pending_topic_subscribes.drain() {
438 let _ = ack.send(Err(eyre!("WS connection closed")));
439 }
440}
441
442fn topic_protocol_mismatch_error(coordinator_version: Option<u16>) -> eyre::Report {
446 eyre!(dora_message::topic_protocol_mismatch_message(
447 "coordinator",
448 coordinator_version
449 ))
450}
451
452#[allow(clippy::too_many_arguments)]
453fn handle_response(
454 id: Uuid,
455 result: Option<Box<serde_json::value::RawValue>>,
456 error: Option<String>,
457 pending_requests: &mut PendingRequests,
458 pending_subscribes: &mut PendingSubscribes,
459 log_subscribers: &mut Vec<std_mpsc::Sender<eyre::Result<Vec<u8>>>>,
460 pending_topic_subscribes: &mut PendingTopicSubscribes,
461 topic_subscribers: &mut TopicSubscribers,
462) {
463 if let Some((ack_tx, log_tx)) = pending_subscribes.remove(&id) {
465 if let Some(error) = error {
466 let _ = ack_tx.send(Err(eyre!("{error}")));
467 } else {
468 log_subscribers.push(log_tx);
469 let _ = ack_tx.send(Ok(()));
470 }
471 return;
472 }
473
474 if let Some((ack_tx, data_tx)) = pending_topic_subscribes.remove(&id) {
476 if let Some(error) = error {
477 let _ = ack_tx.send(Err(eyre!("{error}")));
478 } else if let Some(raw) = &result {
479 let reply: Result<dora_message::coordinator_to_cli::ControlRequestReply, _> =
481 serde_json::from_str(raw.get());
482 match reply {
483 Ok(dora_message::coordinator_to_cli::ControlRequestReply::TopicSubscribed {
484 subscription_id,
485 protocol_version,
486 }) => {
487 if protocol_version != Some(dora_message::TOPIC_DATA_PROTOCOL_VERSION) {
501 let _ = ack_tx.send(Err(topic_protocol_mismatch_error(protocol_version)));
502 } else {
503 topic_subscribers.insert(subscription_id, data_tx);
504 let _ = ack_tx.send(Ok(subscription_id));
505 }
506 }
507 Ok(dora_message::coordinator_to_cli::ControlRequestReply::Error(e)) => {
508 let _ = ack_tx.send(Err(eyre!("{e}")));
509 }
510 _ => {
511 let _ = ack_tx.send(Err(eyre!("unexpected topic subscribe reply")));
512 }
513 }
514 } else {
515 let _ = ack_tx.send(Err(eyre!("empty topic subscribe reply")));
516 }
517 return;
518 }
519
520 if let Some(reply_tx) = pending_requests.remove(&id) {
522 let reply = if let Some(error) = error {
523 let err_reply = dora_message::coordinator_to_cli::ControlRequestReply::Error(error);
525 Ok(serde_json::to_vec(&err_reply).unwrap_or_default())
526 } else if let Some(raw) = result {
527 Ok(raw.get().as_bytes().to_vec())
530 } else {
531 Err(eyre!("empty WS response"))
532 };
533 let _ = reply_tx.send(reply);
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use serde_json::json;
541 use serde_json::value::RawValue;
542
543 fn raw(val: serde_json::Value) -> Box<RawValue> {
544 serde_json::value::to_raw_value(&val).unwrap()
545 }
546
547 #[test]
548 fn handle_response_routes_to_pending() {
549 let id = Uuid::new_v4();
550 let (tx, rx) = oneshot::channel();
551 let mut pending = HashMap::new();
552 pending.insert(id, tx);
553 let mut subscribes = HashMap::new();
554 let mut subs = Vec::new();
555 let mut topic_pending = HashMap::new();
556 let mut topic_subs = HashMap::new();
557
558 handle_response(
559 id,
560 Some(raw(json!({"List": []}))),
561 None,
562 &mut pending,
563 &mut subscribes,
564 &mut subs,
565 &mut topic_pending,
566 &mut topic_subs,
567 );
568
569 let mut rx = rx;
570 let result = rx.try_recv().unwrap().unwrap();
571 let val: serde_json::Value = serde_json::from_slice(&result).unwrap();
572 assert_eq!(val, json!({"List": []}));
573 }
574
575 #[test]
576 fn handle_response_orphan_response() {
577 let id = Uuid::new_v4();
578 let mut pending = HashMap::new();
579 let mut subscribes = HashMap::new();
580 let mut subs = Vec::new();
581 let mut topic_pending = HashMap::new();
582 let mut topic_subs = HashMap::new();
583
584 handle_response(
586 id,
587 Some(raw(json!("ignored"))),
588 None,
589 &mut pending,
590 &mut subscribes,
591 &mut subs,
592 &mut topic_pending,
593 &mut topic_subs,
594 );
595 }
596
597 #[test]
598 fn handle_response_routes_event_to_subscriber() {
599 let id = Uuid::new_v4();
600 let (ack_tx, mut ack_rx) = oneshot::channel();
601 let (log_tx, log_rx) = std_mpsc::channel();
602 let mut pending = HashMap::new();
603 let mut subscribes = HashMap::new();
604 subscribes.insert(id, (ack_tx, log_tx));
605 let mut subs = Vec::new();
606 let mut topic_pending = HashMap::new();
607 let mut topic_subs = HashMap::new();
608
609 handle_response(
611 id,
612 Some(raw(json!({"subscribed": true}))),
613 None,
614 &mut pending,
615 &mut subscribes,
616 &mut subs,
617 &mut topic_pending,
618 &mut topic_subs,
619 );
620
621 assert!(ack_rx.try_recv().unwrap().is_ok());
623 assert_eq!(subs.len(), 1);
625
626 let payload = json!({"message": "test log"});
628 let bytes = serde_json::to_vec(&payload).unwrap();
629 subs[0].send(Ok(bytes.clone())).unwrap();
630 let received = log_rx.recv().unwrap().unwrap();
631 assert_eq!(received, bytes);
632 }
633
634 #[test]
635 fn handle_response_event_no_subscriber() {
636 let id = Uuid::new_v4();
638 let (ack_tx, mut ack_rx) = oneshot::channel();
639 let (log_tx, _log_rx) = std_mpsc::channel();
640 let mut pending = HashMap::new();
641 let mut subscribes = HashMap::new();
642 subscribes.insert(id, (ack_tx, log_tx));
643 let mut subs = Vec::new();
644 let mut topic_pending = HashMap::new();
645 let mut topic_subs = HashMap::new();
646
647 handle_response(
648 id,
649 None,
650 Some("not found".into()),
651 &mut pending,
652 &mut subscribes,
653 &mut subs,
654 &mut topic_pending,
655 &mut topic_subs,
656 );
657
658 assert!(ack_rx.try_recv().unwrap().is_err());
659 assert!(subs.is_empty());
660 }
661
662 #[test]
663 fn handle_response_topic_subscribe_ack() {
664 let id = Uuid::new_v4();
665 let sub_id = Uuid::new_v4();
666 let (ack_tx, mut ack_rx) = oneshot::channel();
667 let (data_tx, _data_rx) = std_mpsc::channel();
668 let mut pending = HashMap::new();
669 let mut subscribes = HashMap::new();
670 let mut subs = Vec::new();
671 let mut topic_pending = HashMap::new();
672 topic_pending.insert(id, (ack_tx, data_tx));
673 let mut topic_subs = HashMap::new();
674
675 handle_response(
676 id,
677 Some(raw(json!({"TopicSubscribed": {
678 "subscription_id": sub_id,
679 "protocol_version": dora_message::TOPIC_DATA_PROTOCOL_VERSION,
680 }}))),
681 None,
682 &mut pending,
683 &mut subscribes,
684 &mut subs,
685 &mut topic_pending,
686 &mut topic_subs,
687 );
688
689 let result_id = ack_rx.try_recv().unwrap().unwrap();
690 assert_eq!(result_id, sub_id);
691 assert!(topic_subs.contains_key(&sub_id));
692 }
693
694 fn ack_with_protocol_version(version: Option<u16>) -> (eyre::Result<Uuid>, bool) {
700 let id = Uuid::new_v4();
701 let sub_id = Uuid::new_v4();
702 let (ack_tx, mut ack_rx) = oneshot::channel();
703 let (data_tx, _data_rx) = std_mpsc::channel();
704 let mut pending = HashMap::new();
705 let mut subscribes = HashMap::new();
706 let mut subs = Vec::new();
707 let mut topic_pending = HashMap::new();
708 topic_pending.insert(id, (ack_tx, data_tx));
709 let mut topic_subs = HashMap::new();
710
711 let result = match version {
712 Some(version) => json!({"TopicSubscribed": {
713 "subscription_id": sub_id, "protocol_version": version,
714 }}),
715 None => json!({"TopicSubscribed": {"subscription_id": sub_id}}),
717 };
718
719 handle_response(
720 id,
721 Some(raw(result)),
722 None,
723 &mut pending,
724 &mut subscribes,
725 &mut subs,
726 &mut topic_pending,
727 &mut topic_subs,
728 );
729
730 (ack_rx.try_recv().unwrap(), topic_subs.contains_key(&sub_id))
731 }
732
733 #[test]
734 fn topic_subscribe_ack_rejects_older_protocol_version() {
735 let (ack, registered) = ack_with_protocol_version(Some(1));
736 let err = ack.expect_err("an older protocol version must be rejected");
737 let msg = format!("{err}");
738 assert!(
739 msg.contains("version 1") && msg.contains("misparse"),
740 "error should name the peer's version and why it is fatal, got: {msg}"
741 );
742 assert!(
743 !registered,
744 "a rejected subscription must not be registered for frame dispatch"
745 );
746 }
747
748 #[test]
749 fn topic_subscribe_ack_rejects_missing_protocol_version() {
750 let (ack, registered) = ack_with_protocol_version(None);
751 let err = ack.expect_err("a pre-handshake coordinator must be rejected");
752 assert!(
753 format!("{err}").contains("predates"),
754 "error should say the coordinator predates the handshake, got: {err}"
755 );
756 assert!(
757 !registered,
758 "a rejected subscription must not be registered for frame dispatch"
759 );
760 }
761
762 #[tokio::test]
763 async fn connect_rejects_from_async_context() {
764 let addr: std::net::SocketAddr = "127.0.0.1:0".parse().unwrap();
765 match WsSession::connect(addr) {
766 Err(err) => assert!(
767 format!("{err}").contains("async context"),
768 "expected 'async context' in error, got: {err}"
769 ),
770 Ok(_) => panic!("expected error from async context"),
771 }
772 }
773
774 #[tokio::test]
775 async fn sender_drop_signals_receiver_error() {
776 let (tx, rx) = oneshot::channel::<eyre::Result<Vec<u8>>>();
779 drop(tx);
780 let result = rx.await;
781 assert!(result.is_err()); }
783}