Skip to main content

foundry_common/provider/
runtime_transport.rs

1//! Runtime transport that connects on first request, which can take either of an HTTP,
2//! WebSocket, or IPC transport. Retries are handled by a client layer (e.g.,
3//! `RetryBackoffLayer`) when used.
4
5use crate::{
6    DEFAULT_USER_AGENT, REQUEST_TIMEOUT,
7    provider::{
8        mpp::transport::{LazyMppHttpTransport, lazy_mpp_ws_connect},
9        redact_url,
10    },
11};
12use alloy_json_rpc::{RequestPacket, ResponsePacket};
13use alloy_pubsub::{PubSubConnect, PubSubFrontend};
14use alloy_rpc_types_engine::{Claims, JwtSecret};
15use alloy_transport::{
16    Authorization, BoxTransport, TransportError, TransportErrorKind, TransportFut,
17    utils::guess_local_url,
18};
19use alloy_transport_ipc::IpcConnect;
20use alloy_transport_ws::WsConnect;
21use regex::{Captures, Regex};
22use reqwest::header::{HeaderName, HeaderValue};
23use std::{
24    error::Error as StdError,
25    fmt,
26    path::PathBuf,
27    str::FromStr,
28    sync::{Arc, LazyLock},
29};
30use thiserror::Error;
31use tokio::{
32    runtime::{Handle, Id},
33    sync::RwLock,
34};
35use tower::Service;
36use url::Url;
37
38/// Known MPP-enabled RPC host suffixes.
39///
40/// Endpoints matching these patterns always use the MPP WebSocket transport,
41/// regardless of whether local MPP keys have been discovered.
42const KNOWN_MPP_HOSTS: &[&str] = &[".mpp.tempo.xyz", ".mpp.moderato.tempo.xyz"];
43
44static HTTP_URL_RE: LazyLock<Regex> =
45    LazyLock::new(|| Regex::new(r#"(?i)https?://[^\s<>"']+"#).expect("valid URL regex"));
46
47/// An enum representing the different transports that can be used to connect to a runtime.
48/// Only meant to be used internally by [RuntimeTransport].
49#[derive(Clone, Debug)]
50pub enum InnerTransport {
51    /// HTTP transport with lazy MPP 402 handling.
52    ///
53    /// For known Tempo endpoints, the MPP layer additionally runs the
54    /// `wallet.tempo.xyz` device-code flow on a 402 when no local access key
55    /// is configured (see [`crate::tempo::ensure_access_key`]).
56    Http(LazyMppHttpTransport),
57    /// WebSocket transport
58    Ws(PubSubFrontend),
59    /// IPC transport
60    Ipc(PubSubFrontend),
61}
62
63/// Error type for the runtime transport.
64#[derive(Error, Debug)]
65pub enum RuntimeTransportError {
66    /// Internal transport error
67    #[error("Internal transport error: {0} with {1}")]
68    TransportError(TransportError, String),
69
70    /// Invalid URL scheme
71    #[error("URL scheme is not supported: {0}")]
72    BadScheme(String),
73
74    /// Invalid HTTP header
75    #[error("Invalid HTTP header: {0}")]
76    BadHeader(String),
77
78    /// Invalid file path
79    #[error("Invalid IPC file path: {0}")]
80    BadPath(String),
81
82    /// Invalid construction of Http provider
83    #[error(transparent)]
84    HttpConstructionError(#[from] reqwest::Error),
85
86    /// Invalid JWT
87    #[error("Invalid JWT: {0}")]
88    InvalidJwt(String),
89}
90
91/// Runtime transport that only connects on first request.
92///
93/// A runtime transport is a custom [`alloy_transport::Transport`] that only connects when the
94/// *first* request is made. When the first request is made, it will connect to the runtime using
95/// either an HTTP WebSocket, or IPC transport depending on the URL used.
96/// Retries for rate-limiting and timeout-related errors are handled by an external
97/// client layer (e.g., `RetryBackoffLayer`) when configured.
98#[derive(Clone, Debug)]
99pub struct RuntimeTransport {
100    /// The inner actual transport used.
101    inner: Arc<RwLock<Option<(Id, InnerTransport)>>>,
102    /// The URL to connect to.
103    url: Url,
104    /// The headers to use for requests.
105    headers: Vec<String>,
106    /// The JWT to use for requests.
107    jwt: Option<String>,
108    /// The timeout for requests.
109    timeout: std::time::Duration,
110    /// Whether to accept invalid certificates.
111    accept_invalid_certs: bool,
112    /// Whether to disable automatic proxy detection.
113    no_proxy: bool,
114}
115
116/// A builder for [RuntimeTransport].
117#[derive(Debug)]
118pub struct RuntimeTransportBuilder {
119    url: Url,
120    headers: Vec<String>,
121    jwt: Option<String>,
122    timeout: std::time::Duration,
123    accept_invalid_certs: bool,
124    no_proxy: bool,
125}
126
127impl RuntimeTransportBuilder {
128    /// Create a new builder with the given URL.
129    pub const fn new(url: Url) -> Self {
130        Self {
131            url,
132            headers: vec![],
133            jwt: None,
134            timeout: REQUEST_TIMEOUT,
135            accept_invalid_certs: false,
136            no_proxy: false,
137        }
138    }
139
140    /// Set the URL for the transport.
141    pub fn with_headers(mut self, headers: Vec<String>) -> Self {
142        self.headers = headers;
143        self
144    }
145
146    /// Set the JWT for the transport.
147    pub fn with_jwt(mut self, jwt: Option<String>) -> Self {
148        self.jwt = jwt;
149        self
150    }
151
152    /// Set the timeout for the transport.
153    pub const fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
154        self.timeout = timeout;
155        self
156    }
157
158    /// Set whether to accept invalid certificates.
159    pub const fn accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
160        self.accept_invalid_certs = accept_invalid_certs;
161        self
162    }
163
164    /// Set whether to disable automatic proxy detection.
165    ///
166    /// This can help in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox)
167    /// where system proxy detection via SCDynamicStore causes crashes.
168    pub const fn no_proxy(mut self, no_proxy: bool) -> Self {
169        self.no_proxy = no_proxy;
170        self
171    }
172
173    /// Builds the [RuntimeTransport] and returns it in a disconnected state.
174    /// The runtime transport will then connect when the first request happens.
175    pub fn build(self) -> RuntimeTransport {
176        RuntimeTransport {
177            inner: Arc::new(RwLock::new(None)),
178            url: self.url,
179            headers: self.headers,
180            jwt: self.jwt,
181            timeout: self.timeout,
182            accept_invalid_certs: self.accept_invalid_certs,
183            no_proxy: self.no_proxy,
184        }
185    }
186}
187
188impl fmt::Display for RuntimeTransport {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "RuntimeTransport {}", redact_url(self.url.as_str()))
191    }
192}
193
194impl RuntimeTransport {
195    /// Connects the underlying transport, depending on the URL scheme.
196    pub async fn connect(&self) -> Result<InnerTransport, RuntimeTransportError> {
197        match self.url.scheme() {
198            "http" | "https" => self.connect_http(),
199            "ws" | "wss" => self.connect_ws().await,
200            "file" => self.connect_ipc().await,
201            _ => Err(RuntimeTransportError::BadScheme(self.url.scheme().to_string())),
202        }
203    }
204
205    fn reqwest_headers(&self) -> Result<reqwest::header::HeaderMap, RuntimeTransportError> {
206        let mut headers = reqwest::header::HeaderMap::new();
207
208        // If there's a JWT, add it to the headers if we can decode it.
209        if let Some(jwt) = self.jwt.clone() {
210            let auth =
211                build_auth(jwt).map_err(|e| RuntimeTransportError::InvalidJwt(e.to_string()))?;
212
213            let mut auth_value: HeaderValue =
214                HeaderValue::from_str(&auth.to_string()).expect("Header should be valid string");
215            auth_value.set_sensitive(true);
216
217            headers.insert(reqwest::header::AUTHORIZATION, auth_value);
218        };
219
220        // Add any custom headers.
221        for header in &self.headers {
222            let make_err = || RuntimeTransportError::BadHeader(header.clone());
223
224            let (key, val) = header.split_once(':').ok_or_else(make_err)?;
225
226            headers.insert(
227                HeaderName::from_str(key.trim()).map_err(|_| make_err())?,
228                HeaderValue::from_str(val.trim()).map_err(|_| make_err())?,
229            );
230        }
231
232        if !headers.contains_key(reqwest::header::USER_AGENT) {
233            headers.insert(
234                reqwest::header::USER_AGENT,
235                HeaderValue::from_str(DEFAULT_USER_AGENT)
236                    .expect("User-Agent should be valid string"),
237            );
238        }
239
240        // If MPP_API_KEY is set, attach it as x-api-key for gated MPP proxies.
241        // Does not override an explicit x-api-key header from the user.
242        if !headers.contains_key(HeaderName::from_static("x-api-key"))
243            && let Ok(api_key) = std::env::var("MPP_API_KEY")
244        {
245            let api_key = api_key.trim();
246            if !api_key.is_empty() {
247                let mut value = HeaderValue::from_str(api_key)
248                    .map_err(|_| RuntimeTransportError::BadHeader("MPP_API_KEY".to_string()))?;
249                value.set_sensitive(true);
250                headers.insert(HeaderName::from_static("x-api-key"), value);
251            }
252        }
253
254        Ok(headers)
255    }
256
257    fn reqwest_client_with_headers(
258        &self,
259        headers: reqwest::header::HeaderMap,
260    ) -> Result<reqwest::Client, RuntimeTransportError> {
261        let mut client_builder = reqwest::Client::builder()
262            .timeout(self.timeout)
263            .danger_accept_invalid_certs(self.accept_invalid_certs);
264
265        // Disable automatic proxy detection if requested. This helps in sandboxed environments
266        // (e.g., Cursor IDE sandbox, macOS App Sandbox) where system proxy detection via
267        // SCDynamicStore causes crashes. See: https://github.com/foundry-rs/foundry/issues/12733
268        if self.no_proxy || guess_local_url(self.url.as_str()) {
269            client_builder = client_builder.no_proxy();
270        }
271
272        client_builder = client_builder.default_headers(headers);
273
274        Ok(client_builder.build()?)
275    }
276
277    /// Creates a new reqwest client from this transport.
278    pub fn reqwest_client(&self) -> Result<reqwest::Client, RuntimeTransportError> {
279        self.reqwest_client_with_headers(self.reqwest_headers()?)
280    }
281
282    /// Connects to an HTTP transport with lazy MPP 402 handling.
283    fn connect_http(&self) -> Result<InnerTransport, RuntimeTransportError> {
284        let headers = self.reqwest_headers()?;
285        let client = self.reqwest_client_with_headers(headers.clone())?;
286        Ok(InnerTransport::Http(LazyMppHttpTransport::lazy(client, self.url.clone(), headers)))
287    }
288
289    /// Connects to a WS transport.
290    ///
291    /// Uses the canonical Alloy MPP WebSocket transport when the endpoint is a
292    /// known MPP service.
293    /// Otherwise falls back to alloy's plain [`WsConnect`] with zero overhead.
294    async fn connect_ws(&self) -> Result<InnerTransport, RuntimeTransportError> {
295        let auth = self
296            .jwt
297            .as_ref()
298            .map(|jwt| build_auth(jwt.clone()))
299            .transpose()
300            .map_err(|e| RuntimeTransportError::InvalidJwt(e.to_string()))?;
301
302        let service = if is_known_mpp_endpoint(&self.url) {
303            let mut ws = lazy_mpp_ws_connect(&self.url);
304            if let Some(auth) = auth {
305                ws = ws.with_auth(auth);
306            }
307            ws.into_service().await.map_err(|e| {
308                RuntimeTransportError::TransportError(e, redact_url(self.url.as_str()))
309            })?
310        } else {
311            let mut ws = WsConnect::new(self.url.to_string());
312            if let Some(auth) = auth {
313                ws = ws.with_auth(auth);
314            }
315            ws.into_service().await.map_err(|e| {
316                RuntimeTransportError::TransportError(e, redact_url(self.url.as_str()))
317            })?
318        };
319
320        Ok(InnerTransport::Ws(service))
321    }
322
323    /// Connects to an IPC transport.
324    async fn connect_ipc(&self) -> Result<InnerTransport, RuntimeTransportError> {
325        let path = url_to_file_path(&self.url)
326            .map_err(|_| RuntimeTransportError::BadPath(self.url.to_string()))?;
327        let ipc_connector = IpcConnect::new(path.clone());
328        let ipc = ipc_connector.into_service().await.map_err(|e| {
329            RuntimeTransportError::TransportError(e, path.clone().display().to_string())
330        })?;
331        Ok(InnerTransport::Ipc(ipc))
332    }
333
334    /// Sends a request using the underlying transport.
335    /// If this is the first request, it will connect to the appropriate transport depending on the
336    /// URL scheme. Retries are performed by an external client layer (e.g., `RetryBackoffLayer`),
337    /// if such a layer is configured by the caller.
338    /// For sending the actual request, this action is delegated down to the
339    /// underlying transport through Tower's [tower::Service::call]. See tower's [tower::Service]
340    /// trait for more information.
341    pub fn request(&self, req: RequestPacket) -> TransportFut<'static> {
342        let this = self.clone();
343        Box::pin(async move {
344            let runtime = Handle::current().id();
345            let inner = this.inner.read().await;
346            let transport = if let Some((id, transport)) = &*inner
347                && *id == runtime
348            {
349                let transport = transport.clone();
350                drop(inner);
351                transport
352            } else {
353                drop(inner);
354                let mut inner = this.inner.write().await;
355                // Connections may own tasks and I/O resources on their originating runtime.
356                // Reconnect when a shared client is used from another runtime.
357                if inner.as_ref().is_none_or(|(id, _)| *id != runtime) {
358                    *inner =
359                        Some((runtime, this.connect().await.map_err(TransportErrorKind::custom)?));
360                }
361                inner.as_ref().expect("transport is connected").1.clone()
362            };
363
364            match transport {
365                InnerTransport::Http(mut http) => http
366                    .call(req)
367                    .await
368                    .map_err(|error| redact_http_transport_error(error, &this.url)),
369                InnerTransport::Ws(mut ws) => ws.call(req).await,
370                InnerTransport::Ipc(mut ipc) => ipc.call(req).await,
371            }
372        })
373    }
374
375    /// Convert this transport into a boxed trait object.
376    pub fn boxed(self) -> BoxTransport {
377        BoxTransport::new(self)
378    }
379}
380
381/// Returns `true` if `url` points to a known MPP-enabled RPC service.
382fn is_known_mpp_endpoint(url: &Url) -> bool {
383    url.host_str().is_some_and(|host| KNOWN_MPP_HOSTS.iter().any(|suffix| host.ends_with(suffix)))
384}
385
386fn redact_http_transport_error(error: TransportError, endpoint: &Url) -> TransportError {
387    let alloy_json_rpc::RpcError::Transport(TransportErrorKind::Custom(source)) = &error else {
388        return error;
389    };
390    let safe_endpoint = redact_url(endpoint.as_str());
391
392    let mut message = String::new();
393    let mut error: Option<&(dyn StdError + 'static)> = Some(source.as_ref());
394    while let Some(source) = error {
395        if !message.is_empty() {
396            message.push_str(": ");
397        }
398        message.push_str(&source.to_string());
399        error = source.source();
400    }
401    let message = HTTP_URL_RE.replace_all(&message, |captures: &Captures<'_>| {
402        let candidate = &captures[0];
403        let Ok(url) = Url::parse(candidate) else { return candidate.to_owned() };
404        if url.host() == endpoint.host()
405            && url.port_or_known_default() == endpoint.port_or_known_default()
406        {
407            safe_endpoint.clone()
408        } else {
409            candidate.to_owned()
410        }
411    });
412    TransportErrorKind::custom_str(&message)
413}
414
415impl tower::Service<RequestPacket> for RuntimeTransport {
416    type Response = ResponsePacket;
417    type Error = TransportError;
418    type Future = TransportFut<'static>;
419
420    #[inline]
421    fn poll_ready(
422        &mut self,
423        _cx: &mut std::task::Context<'_>,
424    ) -> std::task::Poll<Result<(), Self::Error>> {
425        std::task::Poll::Ready(Ok(()))
426    }
427
428    #[inline]
429    fn call(&mut self, req: RequestPacket) -> Self::Future {
430        self.request(req)
431    }
432}
433
434impl tower::Service<RequestPacket> for &RuntimeTransport {
435    type Response = ResponsePacket;
436    type Error = TransportError;
437    type Future = TransportFut<'static>;
438
439    #[inline]
440    fn poll_ready(
441        &mut self,
442        _cx: &mut std::task::Context<'_>,
443    ) -> std::task::Poll<Result<(), Self::Error>> {
444        std::task::Poll::Ready(Ok(()))
445    }
446
447    #[inline]
448    fn call(&mut self, req: RequestPacket) -> Self::Future {
449        self.request(req)
450    }
451}
452
453fn build_auth(jwt: String) -> eyre::Result<Authorization> {
454    // Decode jwt from hex, then generate claims (iat with current timestamp)
455    let secret = JwtSecret::from_hex(jwt)?;
456    let claims = Claims::default();
457    let token = secret.encode(&claims)?;
458
459    let auth = Authorization::Bearer(token);
460
461    Ok(auth)
462}
463
464#[cfg(windows)]
465fn url_to_file_path(url: &Url) -> Result<PathBuf, ()> {
466    const PREFIX: &str = "file:///pipe/";
467
468    let url_str = url.as_str();
469
470    if let Some(pipe_name) = url_str.strip_prefix(PREFIX) {
471        let pipe_path = format!(r"\\.\pipe\{pipe_name}");
472        return Ok(PathBuf::from(pipe_path));
473    }
474
475    url.to_file_path()
476}
477
478#[cfg(not(windows))]
479fn url_to_file_path(url: &Url) -> Result<PathBuf, ()> {
480    url.to_file_path()
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use alloy_json_rpc::{Id as RpcId, Request};
487    use axum::{Json, Router, routing::post};
488    use reqwest::header::HeaderMap;
489    use serde_json::{Value, json};
490    use std::{io, time::Duration};
491    use tokio::{runtime::Builder, sync::Notify, time::timeout};
492
493    #[derive(Debug, Error)]
494    #[error("request to https://example.com/private-api-key failed")]
495    struct ProviderError {
496        #[source]
497        source: io::Error,
498    }
499
500    #[test]
501    fn http_transport_errors_preserve_provider_guidance() {
502        let endpoint =
503            Url::parse("https://user:password@example.com/private-api-key?token=secret").unwrap();
504        let error = TransportErrorKind::custom(ProviderError {
505            source: io::Error::other(
506                "Authorize an access key with:\n  cast tempo login --no-browser",
507            ),
508        });
509
510        let report = redact_http_transport_error(error, &endpoint).to_string();
511
512        assert!(report.contains("https://example.com/"));
513        assert!(report.contains("cast tempo login --no-browser"));
514        assert!(!report.contains("password"));
515        assert!(!report.contains("private-api-key"));
516        assert!(!report.contains("secret"));
517    }
518
519    #[test]
520    fn http_transport_errors_redact_endpoint_paths() {
521        let endpoint =
522            Url::parse("https://user:password@example.com/private-api-key?token=secret").unwrap();
523        let error = TransportErrorKind::custom_str(concat!(
524            "request to https://example.com/private-api-key failed: connection refused\n\n",
525            "Authorize an access key with:\n  cast tempo login"
526        ));
527
528        let error = redact_http_transport_error(error, &endpoint);
529        let report = error.to_string();
530
531        assert!(report.contains("https://example.com/"));
532        assert!(!report.contains("password"));
533        assert!(!report.contains("private-api-key"));
534        assert!(!report.contains("secret"));
535        assert!(report.to_lowercase().contains("connection refused"));
536        assert!(report.contains("cast tempo login"));
537    }
538
539    #[test]
540    fn http_transport_errors_redact_normalized_endpoint_variants() {
541        let endpoint =
542            Url::parse("https://user:password@example.com/private-api-key?token=secret").unwrap();
543        let error = TransportErrorKind::custom_str(
544            "request to https://USER:normalized@example.com:443/different%2Fpath?key=other failed",
545        );
546
547        let report = redact_http_transport_error(error, &endpoint).to_string();
548
549        assert!(report.contains("https://example.com/"));
550        assert!(!report.contains("normalized"));
551        assert!(!report.contains("different"));
552        assert!(!report.contains("other"));
553    }
554
555    #[tokio::test]
556    async fn websocket_error_redacts_url_credentials() {
557        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
558        let address = listener.local_addr().unwrap();
559        drop(listener);
560        let url = Url::parse(&format!(
561            "ws://user:password@{address}/private-api-key?token=secret#fragment"
562        ))
563        .unwrap();
564        let transport = RuntimeTransportBuilder::new(url).build();
565
566        let error = transport.connect_ws().await.unwrap_err().to_string();
567
568        assert!(error.contains(&format!("ws://{address}/")));
569        assert!(!error.contains("user"));
570        assert!(!error.contains("password"));
571        assert!(!error.contains("private-api-key"));
572        assert!(!error.contains("secret"));
573    }
574
575    #[tokio::test]
576    async fn test_user_agent_header() {
577        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
578        let url = Url::parse(&format!("http://{}", listener.local_addr().unwrap())).unwrap();
579
580        let http_handler = axum::routing::get(|actual_headers: HeaderMap| {
581            let user_agent = HeaderName::from_str("User-Agent").unwrap();
582            assert_eq!(actual_headers[user_agent], HeaderValue::from_str("test-agent").unwrap());
583
584            async { "" }
585        });
586
587        let server_task = tokio::spawn(async move {
588            axum::serve(listener, http_handler.into_make_service()).await.unwrap()
589        });
590
591        let transport = RuntimeTransportBuilder::new(url.clone())
592            .with_headers(vec!["User-Agent: test-agent".to_string()])
593            .build();
594        let inner = transport.connect_http().unwrap();
595
596        match inner {
597            InnerTransport::Http(http) => {
598                let _ = http.client().get(url).send().await.unwrap();
599
600                // assert inside http_handler
601            }
602            _ => unreachable!(),
603        }
604
605        server_task.abort();
606    }
607
608    #[tokio::test]
609    async fn connect_rejects_invalid_jwt() {
610        for scheme in ["http", "https", "ws", "wss"] {
611            for jwt in ["not-hex", "00"] {
612                let url = Url::parse(&format!("{scheme}://127.0.0.1:0")).unwrap();
613                let transport =
614                    RuntimeTransportBuilder::new(url).with_jwt(Some(jwt.to_string())).build();
615
616                let error = transport.connect().await.unwrap_err();
617
618                assert!(
619                    matches!(error, RuntimeTransportError::InvalidJwt(_)),
620                    "expected InvalidJwt for {scheme} with {jwt:?}, got {error:?}"
621                );
622            }
623        }
624    }
625
626    #[tokio::test]
627    async fn pending_request_does_not_block_another_runtime() {
628        let started = Arc::new(Notify::new());
629        let release = Arc::new(Notify::new());
630        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
631        let url = Url::parse(&format!("http://{}", listener.local_addr().unwrap())).unwrap();
632        let router = Router::new().route(
633            "/",
634            post({
635                let started = started.clone();
636                let release = release.clone();
637                move |Json(request): Json<Value>| {
638                    let started = started.clone();
639                    let release = release.clone();
640                    async move {
641                        if request["method"] == "hold" {
642                            started.notify_one();
643                            release.notified().await;
644                        }
645                        Json(json!({"jsonrpc": "2.0", "id": request["id"], "result": "0x1"}))
646                    }
647                }
648            }),
649        );
650        let server = tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
651        let transport = RuntimeTransportBuilder::new(url).build();
652        let ready =
653            RequestPacket::from(Request::new("ready", RpcId::Number(1), ()).serialize().unwrap());
654        transport.request(ready.clone()).await.unwrap();
655        let hold = Request::new("hold", RpcId::Number(2), ()).serialize().unwrap().into();
656        let pending = tokio::spawn(transport.request(hold));
657        started.notified().await;
658
659        let result =
660            tokio::task::spawn_blocking(move || {
661                Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
662                    timeout(Duration::from_secs(2), transport.request(ready)).await
663                })
664            })
665            .await
666            .unwrap();
667        release.notify_one();
668        pending.await.unwrap().unwrap();
669        server.abort();
670        result.expect("another runtime waited for an in-flight request").unwrap();
671    }
672}