Skip to main content

fedimint_server/consensus/
iroh_api.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::future::Future;
4use std::panic::AssertUnwindSafe;
5use std::sync::Arc;
6use std::time::Duration;
7
8use fedimint_core::core::ModuleInstanceId;
9use fedimint_core::module::{ApiEndpoint, ApiError, ApiMethod, IrohApiRequest};
10use fedimint_core::task::TaskGroup;
11use fedimint_core::util::FmtCompactAnyhow as _;
12use fedimint_logging::LOG_NET_API;
13use fedimint_metrics::prometheus::HistogramTimer;
14use fedimint_server_core::DynServerModule;
15use futures::FutureExt as _;
16use iroh::Endpoint;
17use iroh::endpoint::{Incoming, RecvStream, SendStream, VarInt};
18use serde_json::Value;
19use tokio::sync::Semaphore;
20use tracing::{error, warn};
21
22use super::api::{ConsensusApi, server_endpoints};
23use crate::connection_limits::ConnectionLimits;
24use crate::metrics::{
25    IROH_API_CONNECTION_DURATION_SECONDS, IROH_API_CONNECTION_IDLE_TIMEOUT_TOTAL,
26    IROH_API_CONNECTIONS_ACTIVE, IROH_API_REQUEST_DURATION_SECONDS, IROH_API_REQUEST_RESPONSE_CODE,
27};
28use crate::net::api::HasApiContext;
29
30/// How long an Iroh API connection may stay idle before the server closes it.
31const IROH_API_CONNECTION_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
32
33/// Application-level QUIC error code for expected idle Iroh API connection
34/// reaping.
35const IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE: u32 = 0;
36
37/// Application-level QUIC close reason for idle Iroh API connection reaping.
38const IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON: &[u8] = b"idle timeout";
39
40/// Metric label used for every request that does not resolve to a registered
41/// endpoint.
42const UNKNOWN_METHOD: &str = "unknown";
43
44pub(super) async fn run_iroh_api(
45    api: Arc<IrohApiState>,
46    endpoint: Endpoint,
47    task_group: TaskGroup,
48) {
49    loop {
50        match endpoint.accept().await {
51            Some(incoming) => {
52                let permit = acquire_iroh_api_permit(
53                    &api.parallel_connections_limit,
54                    api.limits.max_connections,
55                    "0.35",
56                    "connection",
57                )
58                .await;
59                task_group.spawn_cancellable_silent(
60                    "handle-iroh-connection",
61                    handle_incoming(
62                        api.clone(),
63                        task_group.clone(),
64                        incoming,
65                        permit,
66                        api.limits.max_requests_per_connection,
67                    )
68                    .then(|result| async {
69                        if let Err(err) = result {
70                            warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle iroh connection");
71                        }
72                    }),
73                );
74            }
75            None => return,
76        }
77    }
78}
79
80type CoreApi = BTreeMap<String, ApiEndpoint<ConsensusApi>>;
81type ModuleApi = BTreeMap<ModuleInstanceId, BTreeMap<String, ApiEndpoint<DynServerModule>>>;
82
83pub(super) struct IrohApiState {
84    consensus: ConsensusApi,
85    core: CoreApi,
86    modules: ModuleApi,
87    limits: ConnectionLimits,
88    parallel_connections_limit: Arc<Semaphore>,
89}
90
91impl IrohApiState {
92    pub(super) fn new(consensus: ConsensusApi, limits: ConnectionLimits) -> Arc<Self> {
93        let core_api = server_endpoints()
94            .into_iter()
95            .map(|endpoint| (endpoint.path.to_string(), endpoint))
96            .collect();
97
98        let module_api = consensus
99            .modules
100            .iter_modules()
101            .map(|(id, _, module)| {
102                let api_endpoints = module
103                    .api_endpoints()
104                    .into_iter()
105                    .map(|endpoint| (endpoint.path.to_string(), endpoint))
106                    .collect::<BTreeMap<String, ApiEndpoint<DynServerModule>>>();
107
108                (id, api_endpoints)
109            })
110            .collect();
111
112        Arc::new(Self {
113            consensus,
114            core: core_api,
115            modules: module_api,
116            parallel_connections_limit: Arc::new(Semaphore::new(limits.max_connections)),
117            limits,
118        })
119    }
120}
121
122async fn acquire_iroh_api_permit(
123    limit: &Arc<Semaphore>,
124    max: usize,
125    version: &'static str,
126    resource: &'static str,
127) -> tokio::sync::OwnedSemaphorePermit {
128    if limit.available_permits() == 0 {
129        warn!(
130            target: LOG_NET_API,
131            limit = max,
132            version,
133            resource,
134            "Iroh API limit reached, blocking"
135        );
136    }
137    limit
138        .clone()
139        .acquire_owned()
140        .await
141        .expect("semaphore should not be closed")
142}
143
144struct ActiveIrohApiConnection {
145    _duration: HistogramTimer,
146}
147
148impl ActiveIrohApiConnection {
149    fn new() -> Self {
150        IROH_API_CONNECTIONS_ACTIVE.inc();
151        Self {
152            _duration: IROH_API_CONNECTION_DURATION_SECONDS.start_timer(),
153        }
154    }
155}
156
157impl Drop for ActiveIrohApiConnection {
158    fn drop(&mut self) {
159        IROH_API_CONNECTIONS_ACTIVE.dec();
160    }
161}
162
163async fn handle_incoming(
164    api: Arc<IrohApiState>,
165    task_group: TaskGroup,
166    incoming: Incoming,
167    connection_permit: tokio::sync::OwnedSemaphorePermit,
168    iroh_api_max_requests_per_connection: usize,
169) -> anyhow::Result<()> {
170    let connection = incoming.accept()?.await?;
171    handle_iroh_api_connection(
172        api,
173        task_group,
174        VersionedIrohConnection::Legacy(connection),
175        connection_permit,
176        iroh_api_max_requests_per_connection,
177        IrohApiVersion::Legacy,
178    )
179    .await
180}
181
182#[derive(Clone, Copy)]
183enum IrohApiVersion {
184    Legacy,
185    Next,
186}
187
188impl IrohApiVersion {
189    fn log_label(self) -> &'static str {
190        match self {
191            Self::Legacy => "0.35",
192            Self::Next => "1.0",
193        }
194    }
195
196    fn metric_label(self) -> &'static str {
197        match self {
198            Self::Legacy => "default",
199            Self::Next => "next",
200        }
201    }
202
203    fn request_task_name(self) -> &'static str {
204        match self {
205            Self::Legacy => "handle-iroh-request",
206            Self::Next => "handle-iroh-next-request",
207        }
208    }
209}
210
211enum VersionedIrohConnection {
212    Legacy(iroh::endpoint::Connection),
213    Next(iroh_next::endpoint::Connection),
214}
215
216impl VersionedIrohConnection {
217    async fn accept_bi(&self) -> anyhow::Result<(VersionedSendStream, VersionedRecvStream)> {
218        Ok(match self {
219            Self::Legacy(connection) => {
220                let (send, recv) = connection.accept_bi().await?;
221                (
222                    VersionedSendStream::Legacy(send),
223                    VersionedRecvStream::Legacy(recv),
224                )
225            }
226            Self::Next(connection) => {
227                let (send, recv) = connection.accept_bi().await?;
228                (
229                    VersionedSendStream::Next(send),
230                    VersionedRecvStream::Next(recv),
231                )
232            }
233        })
234    }
235
236    fn close_for_idle_timeout(&self) {
237        match self {
238            Self::Legacy(connection) => connection.close(
239                VarInt::from_u32(IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE),
240                IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON,
241            ),
242            Self::Next(connection) => connection.close(
243                iroh_next::endpoint::VarInt::from_u32(IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_CODE),
244                IROH_API_CONNECTION_IDLE_TIMEOUT_ERROR_REASON,
245            ),
246        }
247    }
248}
249
250enum VersionedSendStream {
251    Legacy(SendStream),
252    Next(iroh_next::endpoint::SendStream),
253}
254
255impl VersionedSendStream {
256    async fn write_response(mut self, response: &[u8]) -> anyhow::Result<()> {
257        match &mut self {
258            Self::Legacy(send) => {
259                send.write_all(response).await?;
260                send.finish()?;
261            }
262            Self::Next(send) => {
263                send.write_all(response).await?;
264                send.finish()?;
265            }
266        }
267        Ok(())
268    }
269}
270
271enum VersionedRecvStream {
272    Legacy(RecvStream),
273    Next(iroh_next::endpoint::RecvStream),
274}
275
276impl VersionedRecvStream {
277    async fn read_request(&mut self) -> anyhow::Result<Vec<u8>> {
278        Ok(match self {
279            Self::Legacy(recv) => recv.read_to_end(100_000).await?,
280            Self::Next(recv) => recv.read_to_end(100_000).await?,
281        })
282    }
283}
284
285async fn handle_iroh_api_connection(
286    api: Arc<IrohApiState>,
287    task_group: TaskGroup,
288    connection: VersionedIrohConnection,
289    _connection_permit: tokio::sync::OwnedSemaphorePermit,
290    max_requests: usize,
291    version: IrohApiVersion,
292) -> anyhow::Result<()> {
293    let parallel_requests_limit = Arc::new(Semaphore::new(max_requests));
294    let _metrics = ActiveIrohApiConnection::new();
295
296    loop {
297        let accept_result = fedimint_core::runtime::timeout(
298            IROH_API_CONNECTION_IDLE_TIMEOUT,
299            connection.accept_bi(),
300        )
301        .await;
302
303        let (send_stream, recv_stream) = match accept_result {
304            Ok(streams) => streams?,
305            Err(_) if parallel_requests_limit.available_permits() < max_requests => continue,
306            Err(_) => {
307                IROH_API_CONNECTION_IDLE_TIMEOUT_TOTAL.inc();
308                tracing::debug!(
309                    target: LOG_NET_API,
310                    version = version.log_label(),
311                    idle_timeout_secs = IROH_API_CONNECTION_IDLE_TIMEOUT.as_secs(),
312                    "Closing idle Iroh API connection"
313                );
314                connection.close_for_idle_timeout();
315                return Ok(());
316            }
317        };
318
319        let permit = acquire_iroh_api_permit(
320            &parallel_requests_limit,
321            max_requests,
322            version.log_label(),
323            "request",
324        )
325        .await;
326        task_group.spawn_cancellable_silent(
327            version.request_task_name(),
328            handle_iroh_api_stream(
329                api.clone(),
330                send_stream,
331                recv_stream,
332                permit,
333                version.metric_label(),
334            )
335            .then(|result| async {
336                if let Err(err) = result {
337                    warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle Iroh API request");
338                }
339            }),
340        );
341    }
342}
343
344async fn handle_iroh_api_stream(
345    api: Arc<IrohApiState>,
346    send_stream: VersionedSendStream,
347    mut recv_stream: VersionedRecvStream,
348    _request_permit: tokio::sync::OwnedSemaphorePermit,
349    metric_label: &'static str,
350) -> anyhow::Result<()> {
351    let request = recv_stream.read_request().await?;
352    let response = handle_iroh_api_request(&api, &request, metric_label).await?;
353    send_stream.write_response(&response).await
354}
355
356async fn handle_iroh_api_request(
357    api: &IrohApiState,
358    request: &[u8],
359    version_label: &'static str,
360) -> anyhow::Result<Vec<u8>> {
361    let request = serde_json::from_slice::<IrohApiRequest>(request)?;
362    let request_method = request.method.clone();
363    let response = record_request_metrics(
364        &api.core,
365        &api.modules,
366        &request_method,
367        version_label,
368        await_response(api, request),
369    )
370    .await;
371
372    Ok(serde_json::to_vec(&response)?)
373}
374
375fn metric_method<'a, C, M>(
376    core: &'a BTreeMap<String, C>,
377    modules: &'a BTreeMap<ModuleInstanceId, BTreeMap<String, M>>,
378    method: &ApiMethod,
379) -> Cow<'a, str> {
380    match method {
381        ApiMethod::Core(method) => core
382            .get_key_value(method)
383            .map_or(Cow::Borrowed(UNKNOWN_METHOD), |(method, _)| {
384                Cow::Borrowed(method)
385            }),
386        ApiMethod::Module(module_id, method) => modules
387            .get_key_value(module_id)
388            .and_then(|(module_id, endpoints)| {
389                endpoints
390                    .get_key_value(method)
391                    .map(|(method, _)| Cow::Owned(format!("{module_id}-{method}")))
392            })
393            .unwrap_or(Cow::Borrowed(UNKNOWN_METHOD)),
394    }
395}
396
397async fn record_request_metrics<T, C, M>(
398    core: &BTreeMap<String, C>,
399    modules: &BTreeMap<ModuleInstanceId, BTreeMap<String, M>>,
400    request_method: &ApiMethod,
401    version_label: &'static str,
402    response: impl Future<Output = Result<T, ApiError>>,
403) -> Result<T, ApiError> {
404    let method = metric_method(core, modules, request_method);
405    let timer = IROH_API_REQUEST_DURATION_SECONDS
406        .with_label_values(&[method.as_ref()])
407        .start_timer();
408    let response = response.await;
409    timer.observe_duration();
410
411    let response_code = response
412        .as_ref()
413        .map_or_else(|err| err.code.to_string(), |_| "0".to_string());
414    IROH_API_REQUEST_RESPONSE_CODE
415        .with_label_values(&[method.as_ref(), response_code.as_str(), version_label])
416        .inc();
417
418    response
419}
420
421async fn await_response(api: &IrohApiState, request: IrohApiRequest) -> Result<Value, ApiError> {
422    match request.method {
423        ApiMethod::Core(method) => {
424            let endpoint = api
425                .core
426                .get(&method)
427                .ok_or_else(|| ApiError::not_found(method.clone()))?;
428
429            let (state, context) = api.consensus.context(&request.request, None).await;
430
431            run_handler(
432                None,
433                &method,
434                (endpoint.handler)(state, context, request.request),
435            )
436            .await
437        }
438        ApiMethod::Module(module_id, method) => {
439            let endpoint = api
440                .modules
441                .get(&module_id)
442                .ok_or_else(|| ApiError::not_found(module_id.to_string()))?
443                .get(&method)
444                .ok_or_else(|| ApiError::not_found(method.clone()))?;
445
446            let (state, context) = api
447                .consensus
448                .context(&request.request, Some(module_id))
449                .await;
450
451            run_handler(
452                Some(module_id),
453                &method,
454                (endpoint.handler)(state, context, request.request),
455            )
456            .await
457        }
458    }
459}
460
461/// Runs an API endpoint handler, turning a panic into an error response for the
462/// caller that triggered it.
463///
464/// Iroh API requests run on the root task group, so an escaping panic would
465/// trip the task group's panic guard and shut the whole guardian down. The
466/// jsonrpsee path contains handler panics the same way.
467async fn run_handler(
468    module_id: Option<ModuleInstanceId>,
469    method: &str,
470    handler: impl Future<Output = Result<Value, ApiError>>,
471) -> Result<Value, ApiError> {
472    // Using `AssertUnwindSafe` here is far from ideal. In theory this means we
473    // could end up with an inconsistent state. In practice most API functions are
474    // only reading and the few that do write anything are atomic. Lastly, this is
475    // only the last line of defense.
476    AssertUnwindSafe(handler)
477        .catch_unwind()
478        .await
479        .unwrap_or_else(|_| {
480            error!(
481                target: LOG_NET_API,
482                module_id = ?module_id,
483                method,
484                "API handler panicked, DO NOT IGNORE, FIX IT!!!"
485            );
486
487            Err(ApiError::server_error("API handler panicked".to_string()))
488        })
489}
490
491// --- iroh-next API endpoint functions ---
492
493pub(super) async fn run_iroh_api_next(
494    api: Arc<IrohApiState>,
495    endpoint: iroh_next::Endpoint,
496    task_group: TaskGroup,
497) {
498    loop {
499        match endpoint.accept().await {
500            Some(incoming) => {
501                let permit = acquire_iroh_api_permit(
502                    &api.parallel_connections_limit,
503                    api.limits.max_connections,
504                    "1.0",
505                    "connection",
506                )
507                .await;
508                task_group.spawn_cancellable_silent(
509                    "handle-iroh-next-connection",
510                    handle_incoming_next(
511                        api.clone(),
512                        task_group.clone(),
513                        incoming,
514                        permit,
515                        api.limits.max_requests_per_connection,
516                    )
517                    .then(|result| async {
518                        if let Err(err) = result {
519                            warn!(target: LOG_NET_API, err = %err.fmt_compact_anyhow(), "Failed to handle iroh-next connection");
520                        }
521                    }),
522                );
523            }
524            None => return,
525        }
526    }
527}
528
529async fn handle_incoming_next(
530    api: Arc<IrohApiState>,
531    task_group: TaskGroup,
532    incoming: iroh_next::endpoint::Incoming,
533    connection_permit: tokio::sync::OwnedSemaphorePermit,
534    iroh_api_max_requests_per_connection: usize,
535) -> anyhow::Result<()> {
536    let connection = incoming.accept()?.await?;
537    handle_iroh_api_connection(
538        api,
539        task_group,
540        VersionedIrohConnection::Next(connection),
541        connection_permit,
542        iroh_api_max_requests_per_connection,
543        IrohApiVersion::Next,
544    )
545    .await
546}
547
548#[cfg(test)]
549mod tests {
550    use std::collections::BTreeSet;
551    use std::net::SocketAddr;
552
553    use anyhow::Context as _;
554    use fedimint_metrics::prometheus::core::Collector;
555    use fedimint_metrics::prometheus::proto::Metric;
556    use futures::future::pending;
557    use futures::{pin_mut, poll};
558    use iroh_next::endpoint::presets::Minimal;
559    use iroh_next::{EndpointAddr, RelayMode, SecretKey, TransportAddr};
560
561    use super::*;
562
563    const TEST_ALPN: &[u8] = b"fedimint-iroh-api-adapter-test";
564
565    fn has_method_label(metric: &Metric, method: &str) -> bool {
566        metric
567            .get_label()
568            .iter()
569            .any(|label| label.name() == "method" && label.value() == method)
570    }
571
572    fn duration_count(method: &str) -> u64 {
573        IROH_API_REQUEST_DURATION_SECONDS
574            .collect()
575            .into_iter()
576            .flat_map(|family| family.metric)
577            .filter(|metric| has_method_label(metric, method))
578            .map(|metric| metric.histogram.sample_count())
579            .sum()
580    }
581
582    fn response_count(method: &str) -> u64 {
583        IROH_API_REQUEST_RESPONSE_CODE
584            .collect()
585            .into_iter()
586            .flat_map(|family| family.metric)
587            .filter(|metric| has_method_label(metric, method))
588            .map(|metric| metric.counter.value() as u64)
589            .sum()
590    }
591
592    fn method_series(metrics: Vec<Metric>, method: &str) -> BTreeSet<Vec<(String, String)>> {
593        metrics
594            .into_iter()
595            .filter(|metric| has_method_label(metric, method))
596            .map(|metric| {
597                metric
598                    .label
599                    .into_iter()
600                    .map(|label| (label.name().to_owned(), label.value().to_owned()))
601                    .collect()
602            })
603            .collect()
604    }
605
606    fn duration_series(method: &str) -> BTreeSet<Vec<(String, String)>> {
607        method_series(
608            IROH_API_REQUEST_DURATION_SECONDS
609                .collect()
610                .into_iter()
611                .flat_map(|family| family.metric)
612                .collect(),
613            method,
614        )
615    }
616
617    fn response_series(method: &str) -> BTreeSet<Vec<(String, String)>> {
618        method_series(
619            IROH_API_REQUEST_RESPONSE_CODE
620                .collect()
621                .into_iter()
622                .flat_map(|family| family.metric)
623                .collect(),
624            method,
625        )
626    }
627
628    #[tokio::test]
629    async fn bounds_method_labels_for_all_iroh_request_metrics() {
630        const CORE_METHOD: &str = "metrics_test_core";
631        const MODULE_ID: ModuleInstanceId = 42;
632        const MODULE_METHOD: &str = "metrics_test_module";
633        const MODULE_LABEL: &str = "42-metrics_test_module";
634
635        let core = BTreeMap::from([(CORE_METHOD.to_owned(), true)]);
636        let modules = BTreeMap::from([(
637            MODULE_ID,
638            BTreeMap::from([(MODULE_METHOD.to_owned(), true)]),
639        )]);
640        let hostile_methods = [
641            ApiMethod::Core("metrics_test_unknown_core".to_owned()),
642            ApiMethod::Core("metrics_test_unknown_core_!@#$%^&*()".to_owned()),
643            ApiMethod::Module(MODULE_ID, "metrics_test_unknown_module_method".to_owned()),
644            ApiMethod::Module(
645                MODULE_ID,
646                "metrics_test_unknown_module_method_with_a_long_suffix".to_owned(),
647            ),
648            ApiMethod::Module(43, "metrics_test_unknown_module".to_owned()),
649            ApiMethod::Module(44, "metrics_test_unknown_module_!@#$%^&*()".to_owned()),
650        ];
651
652        assert_eq!(
653            metric_method(&core, &modules, &ApiMethod::Core(CORE_METHOD.to_owned())),
654            CORE_METHOD
655        );
656        assert_eq!(
657            metric_method(
658                &core,
659                &modules,
660                &ApiMethod::Module(MODULE_ID, MODULE_METHOD.to_owned())
661            ),
662            MODULE_LABEL
663        );
664        for method in &hostile_methods {
665            assert_eq!(metric_method(&core, &modules, method), UNKNOWN_METHOD);
666        }
667
668        let duration_before = [
669            duration_count(CORE_METHOD),
670            duration_count(MODULE_LABEL),
671            duration_count(UNKNOWN_METHOD),
672        ];
673        let response_before = [
674            response_count(CORE_METHOD),
675            response_count(MODULE_LABEL),
676            response_count(UNKNOWN_METHOD),
677        ];
678
679        record_request_metrics(
680            &core,
681            &modules,
682            &ApiMethod::Core(CORE_METHOD.to_owned()),
683            "default",
684            async { Ok::<_, ApiError>(()) },
685        )
686        .await
687        .expect("registered core request succeeds");
688        record_request_metrics(
689            &core,
690            &modules,
691            &ApiMethod::Module(MODULE_ID, MODULE_METHOD.to_owned()),
692            "next",
693            async { Ok::<_, ApiError>(()) },
694        )
695        .await
696        .expect("registered module request succeeds");
697        for method in &hostile_methods {
698            record_request_metrics(&core, &modules, method, "default", async {
699                Err::<(), _>(ApiError::not_found("test rejection".to_owned()))
700            })
701            .await
702            .expect_err("unregistered request is rejected");
703        }
704
705        {
706            let cancelled_method =
707                ApiMethod::Core("metrics_test_cancelled_attacker_input".to_owned());
708            let cancelled = record_request_metrics(
709                &core,
710                &modules,
711                &cancelled_method,
712                "default",
713                pending::<Result<(), ApiError>>(),
714            );
715            pin_mut!(cancelled);
716            assert!(poll!(cancelled.as_mut()).is_pending());
717        }
718
719        assert_eq!(duration_count(CORE_METHOD) - duration_before[0], 1);
720        assert_eq!(duration_count(MODULE_LABEL) - duration_before[1], 1);
721        assert_eq!(
722            duration_count(UNKNOWN_METHOD) - duration_before[2],
723            hostile_methods.len() as u64 + 1
724        );
725        assert_eq!(response_count(CORE_METHOD) - response_before[0], 1);
726        assert_eq!(response_count(MODULE_LABEL) - response_before[1], 1);
727        assert_eq!(
728            response_count(UNKNOWN_METHOD) - response_before[2],
729            hostile_methods.len() as u64
730        );
731        assert_eq!(duration_series(UNKNOWN_METHOD).len(), 1);
732        let unknown_response_series = response_series(UNKNOWN_METHOD);
733        assert_eq!(unknown_response_series.len(), 1);
734        assert!(unknown_response_series.iter().any(|labels| {
735            labels
736                .iter()
737                .any(|(name, value)| name == "code" && value == "404")
738                && labels
739                    .iter()
740                    .any(|(name, value)| name == "type" && value == "default")
741        }));
742        assert!(response_series(CORE_METHOD).iter().any(|labels| {
743            labels
744                .iter()
745                .any(|(name, value)| name == "code" && value == "0")
746                && labels
747                    .iter()
748                    .any(|(name, value)| name == "type" && value == "default")
749        }));
750        assert!(response_series(MODULE_LABEL).iter().any(|labels| {
751            labels
752                .iter()
753                .any(|(name, value)| name == "code" && value == "0")
754                && labels
755                    .iter()
756                    .any(|(name, value)| name == "type" && value == "next")
757        }));
758
759        for method in hostile_methods
760            .iter()
761            .map(ToString::to_string)
762            .chain(["metrics_test_cancelled_attacker_input".to_owned()])
763        {
764            assert!(duration_series(&method).is_empty());
765            assert!(response_series(&method).is_empty());
766        }
767    }
768
769    #[tokio::test]
770    async fn panicking_handler_returns_an_error_instead_of_unwinding() {
771        let error = run_handler(None, "test_endpoint", async { panic!("handler panic") })
772            .await
773            .expect_err("a panicking handler is reported as a server error");
774
775        assert_eq!(error.code, 500);
776
777        let error = run_handler(Some(3), "test_endpoint", async {
778            panic!("module handler panic")
779        })
780        .await
781        .expect_err("a panicking module handler is reported as a server error");
782
783        assert_eq!(error.code, 500);
784    }
785
786    #[tokio::test]
787    async fn shared_connection_limit_applies_across_versions() {
788        let limit = Arc::new(Semaphore::new(1));
789        let legacy_permit = acquire_iroh_api_permit(&limit, 1, "0.35", "connection").await;
790
791        assert!(
792            tokio::time::timeout(
793                Duration::from_millis(20),
794                acquire_iroh_api_permit(&limit, 1, "1.0", "connection"),
795            )
796            .await
797            .is_err()
798        );
799
800        drop(legacy_permit);
801        let _permit = tokio::time::timeout(
802            Duration::from_secs(1),
803            acquire_iroh_api_permit(&limit, 1, "1.0", "connection"),
804        )
805        .await
806        .expect("v1 acquires the shared permit after the legacy connection releases it");
807    }
808
809    #[tokio::test]
810    async fn iroh_v1_request_uses_shared_stream_adapter() -> anyhow::Result<()> {
811        let server = iroh_next::Endpoint::builder(Minimal)
812            .relay_mode(RelayMode::Disabled)
813            .secret_key(SecretKey::from_bytes(&[11; 32]))
814            .alpns(vec![TEST_ALPN.to_vec()])
815            .bind_addr(SocketAddr::from(([127, 0, 0, 1], 0)))?
816            .bind()
817            .await?;
818        let client = iroh_next::Endpoint::builder(Minimal)
819            .relay_mode(RelayMode::Disabled)
820            .bind()
821            .await?;
822        let server_addr = EndpointAddr::from_parts(
823            server.id(),
824            server.bound_sockets().into_iter().map(TransportAddr::Ip),
825        );
826        let (client_done_tx, client_done_rx) = tokio::sync::oneshot::channel();
827
828        let server_request = async {
829            let incoming = server.accept().await.context("server endpoint closed")?;
830            let connection = incoming.accept()?.await?;
831            let (send, mut recv) = VersionedIrohConnection::Next(connection)
832                .accept_bi()
833                .await?;
834            assert_eq!(recv.read_request().await?, b"request");
835            send.write_response(b"response").await?;
836            client_done_rx.await?;
837            anyhow::Ok(())
838        };
839        let client_request = async {
840            let connection = client.connect(server_addr, TEST_ALPN).await?;
841            let (mut send, mut recv) = connection.open_bi().await?;
842            send.write_all(b"request").await?;
843            send.finish()?;
844            let response = recv.read_to_end(100_000).await?;
845            anyhow::ensure!(response == b"response");
846            client_done_tx.send(()).expect("server is still running");
847            anyhow::Ok(())
848        };
849
850        tokio::time::timeout(Duration::from_secs(10), async {
851            tokio::try_join!(server_request, client_request)
852        })
853        .await
854        .context("Iroh v1 adapter test timed out")??;
855        client.close().await;
856        server.close().await;
857        Ok(())
858    }
859}