mithril_client/
client.rs

1use anyhow::{Context, anyhow};
2#[cfg(feature = "fs")]
3use chrono::Utc;
4use reqwest::Url;
5use serde::{Deserialize, Serialize};
6use slog::{Logger, o};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10use mithril_common::api_version::APIVersionProvider;
11use mithril_common::{MITHRIL_CLIENT_TYPE_HEADER, MITHRIL_ORIGIN_TAG_HEADER};
12
13use crate::MithrilResult;
14use crate::aggregator_client::{AggregatorClient, AggregatorHTTPClient};
15use crate::cardano_database_client::CardanoDatabaseClient;
16use crate::cardano_stake_distribution_client::CardanoStakeDistributionClient;
17use crate::cardano_transaction_client::CardanoTransactionClient;
18#[cfg(feature = "unstable")]
19use crate::certificate_client::CertificateVerifierCache;
20use crate::certificate_client::{
21    CertificateClient, CertificateVerifier, MithrilCertificateVerifier,
22};
23use crate::era::{AggregatorHttpEraFetcher, EraFetcher, MithrilEraClient};
24use crate::feedback::{FeedbackReceiver, FeedbackSender};
25#[cfg(feature = "fs")]
26use crate::file_downloader::{
27    FileDownloadRetryPolicy, FileDownloader, HttpFileDownloader, RetryDownloader,
28};
29use crate::mithril_stake_distribution_client::MithrilStakeDistributionClient;
30use crate::snapshot_client::SnapshotClient;
31#[cfg(feature = "fs")]
32use crate::utils::AncillaryVerifier;
33#[cfg(feature = "fs")]
34use crate::utils::TimestampTempDirectoryProvider;
35
36const DEFAULT_CLIENT_TYPE: &str = "LIBRARY";
37
38#[cfg(target_family = "wasm")]
39const fn one_week_in_seconds() -> u32 {
40    604800
41}
42
43/// Options that can be used to configure the client.
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct ClientOptions {
46    /// HTTP headers to include in the client requests.
47    pub http_headers: Option<HashMap<String, String>>,
48
49    /// Tag to retrieve the origin of the client requests.
50    #[cfg(target_family = "wasm")]
51    #[cfg_attr(target_family = "wasm", serde(default))]
52    pub origin_tag: Option<String>,
53
54    /// Whether to enable unstable features in the WASM client.
55    #[cfg(target_family = "wasm")]
56    #[cfg_attr(target_family = "wasm", serde(default))]
57    pub unstable: bool,
58
59    /// Whether to enable certificate chain verification caching in the WASM client.
60    ///
61    /// `unstable` must be set to `true` for this option to have any effect.
62    ///
63    /// DANGER: This feature is highly experimental and insecure, and it must not be used in production
64    #[cfg(target_family = "wasm")]
65    #[cfg_attr(target_family = "wasm", serde(default))]
66    pub enable_certificate_chain_verification_cache: bool,
67
68    /// Duration in seconds of certificate chain verification cache in the WASM client.
69    ///
70    /// Default to one week (604800 seconds).
71    ///
72    /// `enable_certificate_chain_verification_cache` and `unstable` must both be set to `true`
73    /// for this option to have any effect.
74    #[cfg(target_family = "wasm")]
75    #[cfg_attr(target_family = "wasm", serde(default = "one_week_in_seconds"))]
76    pub certificate_chain_verification_cache_duration_in_seconds: u32,
77}
78
79impl ClientOptions {
80    /// Instantiate a new [ClientOptions].
81    pub fn new(http_headers: Option<HashMap<String, String>>) -> Self {
82        Self {
83            http_headers,
84            #[cfg(target_family = "wasm")]
85            origin_tag: None,
86            #[cfg(target_family = "wasm")]
87            unstable: false,
88            #[cfg(target_family = "wasm")]
89            enable_certificate_chain_verification_cache: false,
90            #[cfg(target_family = "wasm")]
91            certificate_chain_verification_cache_duration_in_seconds: one_week_in_seconds(),
92        }
93    }
94
95    /// Enable unstable features in the WASM client.
96    #[cfg(target_family = "wasm")]
97    pub fn with_unstable_features(self, unstable: bool) -> Self {
98        Self { unstable, ..self }
99    }
100}
101
102/// Structure that aggregates the available clients for each of the Mithril types of certified data.
103///
104/// Use the [ClientBuilder] to instantiate it easily.
105#[derive(Clone)]
106pub struct Client {
107    certificate_client: Arc<CertificateClient>,
108    mithril_stake_distribution_client: Arc<MithrilStakeDistributionClient>,
109    snapshot_client: Arc<SnapshotClient>,
110    cardano_database_client: Arc<CardanoDatabaseClient>,
111    cardano_transaction_client: Arc<CardanoTransactionClient>,
112    cardano_stake_distribution_client: Arc<CardanoStakeDistributionClient>,
113    mithril_era_client: Arc<MithrilEraClient>,
114}
115
116impl Client {
117    /// Get the client that fetches and verifies Mithril certificates.
118    pub fn certificate(&self) -> Arc<CertificateClient> {
119        self.certificate_client.clone()
120    }
121
122    /// Get the client that fetches Mithril stake distributions.
123    pub fn mithril_stake_distribution(&self) -> Arc<MithrilStakeDistributionClient> {
124        self.mithril_stake_distribution_client.clone()
125    }
126
127    #[deprecated(since = "0.11.9", note = "supersede by `cardano_database`")]
128    /// Get the client that fetches and downloads Mithril snapshots.
129    pub fn snapshot(&self) -> Arc<SnapshotClient> {
130        self.cardano_database()
131    }
132
133    /// Get the client that fetches and downloads Mithril snapshots.
134    pub fn cardano_database(&self) -> Arc<SnapshotClient> {
135        self.snapshot_client.clone()
136    }
137
138    /// Get the client that fetches and downloads Cardano database snapshots.
139    pub fn cardano_database_v2(&self) -> Arc<CardanoDatabaseClient> {
140        self.cardano_database_client.clone()
141    }
142
143    /// Get the client that fetches and verifies Mithril Cardano transaction proof.
144    pub fn cardano_transaction(&self) -> Arc<CardanoTransactionClient> {
145        self.cardano_transaction_client.clone()
146    }
147
148    /// Get the client that fetches Cardano stake distributions.
149    pub fn cardano_stake_distribution(&self) -> Arc<CardanoStakeDistributionClient> {
150        self.cardano_stake_distribution_client.clone()
151    }
152
153    /// Get the client that fetches the current Mithril era.
154    pub fn mithril_era_client(&self) -> Arc<MithrilEraClient> {
155        self.mithril_era_client.clone()
156    }
157}
158
159/// Builder than can be used to create a [Client] easily or with custom dependencies.
160pub struct ClientBuilder {
161    aggregator_endpoint: Option<String>,
162    genesis_verification_key: String,
163    origin_tag: Option<String>,
164    client_type: Option<String>,
165    #[cfg(feature = "fs")]
166    ancillary_verification_key: Option<String>,
167    aggregator_client: Option<Arc<dyn AggregatorClient>>,
168    certificate_verifier: Option<Arc<dyn CertificateVerifier>>,
169    #[cfg(feature = "fs")]
170    http_file_downloader: Option<Arc<dyn FileDownloader>>,
171    #[cfg(feature = "unstable")]
172    certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
173    era_fetcher: Option<Arc<dyn EraFetcher>>,
174    logger: Option<Logger>,
175    feedback_receivers: Vec<Arc<dyn FeedbackReceiver>>,
176    options: ClientOptions,
177}
178
179impl ClientBuilder {
180    /// Constructs a new `ClientBuilder` that fetches data from the aggregator at the given
181    /// endpoint and with the given genesis verification key.
182    pub fn aggregator(endpoint: &str, genesis_verification_key: &str) -> ClientBuilder {
183        Self {
184            aggregator_endpoint: Some(endpoint.to_string()),
185            genesis_verification_key: genesis_verification_key.to_string(),
186            origin_tag: None,
187            client_type: None,
188            #[cfg(feature = "fs")]
189            ancillary_verification_key: None,
190            aggregator_client: None,
191            certificate_verifier: None,
192            #[cfg(feature = "fs")]
193            http_file_downloader: None,
194            #[cfg(feature = "unstable")]
195            certificate_verifier_cache: None,
196            era_fetcher: None,
197            logger: None,
198            feedback_receivers: vec![],
199            options: ClientOptions::default(),
200        }
201    }
202
203    /// Constructs a new `ClientBuilder` without any dependency set.
204    ///
205    /// Use [ClientBuilder::aggregator] if you don't need to set a custom [AggregatorClient]
206    /// to request data from the aggregator.
207    #[deprecated(since = "0.12.33", note = "Will be removed in 0.13.0")]
208    pub fn new(genesis_verification_key: &str) -> ClientBuilder {
209        Self {
210            aggregator_endpoint: None,
211            genesis_verification_key: genesis_verification_key.to_string(),
212            origin_tag: None,
213            client_type: None,
214            #[cfg(feature = "fs")]
215            ancillary_verification_key: None,
216            aggregator_client: None,
217            certificate_verifier: None,
218            #[cfg(feature = "fs")]
219            http_file_downloader: None,
220            #[cfg(feature = "unstable")]
221            certificate_verifier_cache: None,
222            era_fetcher: None,
223            logger: None,
224            feedback_receivers: vec![],
225            options: ClientOptions::default(),
226        }
227    }
228
229    /// Returns a `Client` that uses the dependencies provided to this `ClientBuilder`.
230    ///
231    /// The builder will try to create the missing dependencies using default implementations
232    /// if possible.
233    pub fn build(self) -> MithrilResult<Client> {
234        let logger = self
235            .logger
236            .clone()
237            .unwrap_or_else(|| Logger::root(slog::Discard, o!()));
238
239        let feedback_sender = FeedbackSender::new(&self.feedback_receivers);
240
241        let aggregator_client = match self.aggregator_client {
242            None => Arc::new(self.build_aggregator_client(logger.clone())?),
243            Some(client) => client,
244        };
245
246        let mithril_era_client = match self.era_fetcher {
247            None => Arc::new(MithrilEraClient::new(Arc::new(
248                AggregatorHttpEraFetcher::new(aggregator_client.clone()),
249            ))),
250            Some(era_fetcher) => Arc::new(MithrilEraClient::new(era_fetcher)),
251        };
252
253        let certificate_verifier = match self.certificate_verifier {
254            None => Arc::new(
255                MithrilCertificateVerifier::new(
256                    aggregator_client.clone(),
257                    &self.genesis_verification_key,
258                    feedback_sender.clone(),
259                    #[cfg(feature = "unstable")]
260                    self.certificate_verifier_cache,
261                    logger.clone(),
262                )
263                .with_context(|| "Building certificate verifier failed")?,
264            ),
265            Some(verifier) => verifier,
266        };
267        let certificate_client = Arc::new(CertificateClient::new(
268            aggregator_client.clone(),
269            certificate_verifier,
270            logger.clone(),
271        ));
272
273        let mithril_stake_distribution_client = Arc::new(MithrilStakeDistributionClient::new(
274            aggregator_client.clone(),
275        ));
276
277        #[cfg(feature = "fs")]
278        let http_file_downloader = match self.http_file_downloader {
279            None => Arc::new(RetryDownloader::new(
280                Arc::new(
281                    HttpFileDownloader::new(feedback_sender.clone(), logger.clone())
282                        .with_context(|| "Building http file downloader failed")?,
283                ),
284                FileDownloadRetryPolicy::default(),
285            )),
286            Some(http_file_downloader) => http_file_downloader,
287        };
288
289        #[cfg(feature = "fs")]
290        let ancillary_verifier = match self.ancillary_verification_key {
291            None => None,
292            Some(verification_key) => Some(Arc::new(AncillaryVerifier::new(
293                verification_key
294                    .try_into()
295                    .with_context(|| "Building ancillary verifier failed")?,
296            ))),
297        };
298
299        let snapshot_client = Arc::new(SnapshotClient::new(
300            aggregator_client.clone(),
301            #[cfg(feature = "fs")]
302            http_file_downloader.clone(),
303            #[cfg(feature = "fs")]
304            ancillary_verifier.clone(),
305            #[cfg(feature = "fs")]
306            feedback_sender.clone(),
307            #[cfg(feature = "fs")]
308            logger.clone(),
309        ));
310
311        let cardano_database_client = Arc::new(CardanoDatabaseClient::new(
312            aggregator_client.clone(),
313            #[cfg(feature = "fs")]
314            http_file_downloader,
315            #[cfg(feature = "fs")]
316            ancillary_verifier,
317            #[cfg(feature = "fs")]
318            feedback_sender,
319            #[cfg(feature = "fs")]
320            Arc::new(TimestampTempDirectoryProvider::new(&format!(
321                "{}",
322                Utc::now().timestamp_micros()
323            ))),
324            #[cfg(feature = "fs")]
325            logger,
326        ));
327
328        let cardano_transaction_client =
329            Arc::new(CardanoTransactionClient::new(aggregator_client.clone()));
330
331        let cardano_stake_distribution_client =
332            Arc::new(CardanoStakeDistributionClient::new(aggregator_client));
333
334        Ok(Client {
335            certificate_client,
336            mithril_stake_distribution_client,
337            snapshot_client,
338            cardano_database_client,
339            cardano_transaction_client,
340            cardano_stake_distribution_client,
341            mithril_era_client,
342        })
343    }
344
345    fn build_aggregator_client(
346        &self,
347        logger: Logger,
348    ) -> Result<AggregatorHTTPClient, anyhow::Error> {
349        let endpoint = self
350            .aggregator_endpoint.as_ref()
351            .ok_or(anyhow!("No aggregator endpoint set: \
352                    You must either provide an aggregator endpoint or your own AggregatorClient implementation"))?;
353        let endpoint_url = Url::parse(endpoint).with_context(|| {
354            format!("Invalid aggregator endpoint, it must be a correctly formed url: '{endpoint}'")
355        })?;
356
357        let headers = self.compute_http_headers();
358
359        AggregatorHTTPClient::new(
360            endpoint_url,
361            APIVersionProvider::compute_all_versions_sorted(),
362            logger,
363            Some(headers),
364        )
365        .with_context(|| "Building aggregator client failed")
366    }
367
368    fn compute_http_headers(&self) -> HashMap<String, String> {
369        let mut headers = self.options.http_headers.clone().unwrap_or_default();
370        if let Some(origin_tag) = self.origin_tag.clone() {
371            headers.insert(MITHRIL_ORIGIN_TAG_HEADER.to_string(), origin_tag);
372        }
373        if let Some(client_type) = self.client_type.clone() {
374            headers.insert(MITHRIL_CLIENT_TYPE_HEADER.to_string(), client_type);
375        } else if !headers.contains_key(MITHRIL_CLIENT_TYPE_HEADER) {
376            headers.insert(
377                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
378                DEFAULT_CLIENT_TYPE.to_string(),
379            );
380        }
381
382        headers
383    }
384
385    /// Set the [AggregatorClient] that will be used to request data to the aggregator.
386    #[deprecated(since = "0.12.33", note = "Will be removed in 0.13.0")]
387    pub fn with_aggregator_client(
388        mut self,
389        aggregator_client: Arc<dyn AggregatorClient>,
390    ) -> ClientBuilder {
391        self.aggregator_client = Some(aggregator_client);
392        self
393    }
394
395    /// Sets the [EraFetcher] that will be used by the client to retrieve the current Mithril era.
396    pub fn with_era_fetcher(mut self, era_fetcher: Arc<dyn EraFetcher>) -> ClientBuilder {
397        self.era_fetcher = Some(era_fetcher);
398        self
399    }
400
401    /// Set the [CertificateVerifier] that will be used to validate certificates.
402    pub fn with_certificate_verifier(
403        mut self,
404        certificate_verifier: Arc<dyn CertificateVerifier>,
405    ) -> ClientBuilder {
406        self.certificate_verifier = Some(certificate_verifier);
407        self
408    }
409
410    cfg_unstable! {
411        /// Set the [CertificateVerifierCache] that will be used to cache certificate validation results.
412        ///
413        /// Passing a `None` value will disable the cache if any was previously set.
414        pub fn with_certificate_verifier_cache(
415            mut self,
416            certificate_verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
417        ) -> ClientBuilder {
418            self.certificate_verifier_cache = certificate_verifier_cache;
419            self
420        }
421    }
422
423    cfg_fs! {
424        /// Set the [FileDownloader] that will be used to download artifacts with HTTP.
425        pub fn with_http_file_downloader(
426            mut self,
427            http_file_downloader: Arc<dyn FileDownloader>,
428        ) -> ClientBuilder {
429            self.http_file_downloader = Some(http_file_downloader);
430            self
431        }
432
433        /// Set the ancillary verification key to use when verifying the downloaded ancillary files.
434        pub fn set_ancillary_verification_key<T: Into<Option<String>>>(
435            mut self,
436            ancillary_verification_key: T,
437        ) -> ClientBuilder {
438            self.ancillary_verification_key = ancillary_verification_key.into();
439            self
440        }
441    }
442
443    /// Set the [Logger] to use.
444    pub fn with_logger(mut self, logger: Logger) -> Self {
445        self.logger = Some(logger);
446        self
447    }
448
449    /// Set the origin tag.
450    pub fn with_origin_tag(mut self, origin_tag: Option<String>) -> Self {
451        self.origin_tag = origin_tag;
452        self
453    }
454
455    /// Set the client type.
456    pub fn with_client_type(mut self, client_type: Option<String>) -> Self {
457        self.client_type = client_type;
458        self
459    }
460
461    /// Sets the options to be used by the client.
462    pub fn with_options(mut self, options: ClientOptions) -> Self {
463        self.options = options;
464        self
465    }
466
467    /// Add a [feedback receiver][FeedbackReceiver] to receive [events][crate::feedback::MithrilEvent]
468    /// for tasks that can have a long duration (ie: snapshot download or a long certificate chain
469    /// validation).
470    pub fn add_feedback_receiver(mut self, receiver: Arc<dyn FeedbackReceiver>) -> Self {
471        self.feedback_receivers.push(receiver);
472        self
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    fn default_headers() -> HashMap<String, String> {
481        HashMap::from([(
482            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
483            DEFAULT_CLIENT_TYPE.to_string(),
484        )])
485    }
486
487    #[tokio::test]
488    async fn compute_http_headers_returns_options_http_headers() {
489        let http_headers = default_headers();
490        let client_builder = ClientBuilder::aggregator("", "").with_options(ClientOptions {
491            http_headers: Some(http_headers.clone()),
492        });
493
494        let computed_headers = client_builder.compute_http_headers();
495
496        assert_eq!(computed_headers, http_headers);
497    }
498
499    #[tokio::test]
500    async fn compute_http_headers_with_origin_tag_returns_options_http_headers_with_origin_tag() {
501        let http_headers = default_headers();
502        let client_builder = ClientBuilder::aggregator("", "")
503            .with_options(ClientOptions {
504                http_headers: Some(http_headers.clone()),
505            })
506            .with_origin_tag(Some("CLIENT_TAG".to_string()));
507        let mut expected_headers = http_headers.clone();
508        expected_headers.insert(
509            MITHRIL_ORIGIN_TAG_HEADER.to_string(),
510            "CLIENT_TAG".to_string(),
511        );
512
513        let computed_headers = client_builder.compute_http_headers();
514        assert_eq!(computed_headers, expected_headers);
515    }
516
517    #[tokio::test]
518    async fn test_with_origin_tag_not_overwrite_other_client_options_attributes() {
519        let builder = ClientBuilder::aggregator("", "")
520            .with_options(ClientOptions { http_headers: None })
521            .with_origin_tag(Some("TEST".to_string()));
522        assert_eq!(None, builder.options.http_headers);
523        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
524
525        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
526        let builder = ClientBuilder::aggregator("", "")
527            .with_options(ClientOptions {
528                http_headers: Some(http_headers.clone()),
529            })
530            .with_origin_tag(Some("TEST".to_string()));
531        assert_eq!(Some(http_headers), builder.options.http_headers);
532        assert_eq!(Some("TEST".to_string()), builder.origin_tag);
533    }
534
535    #[tokio::test]
536    async fn test_with_origin_tag_can_be_unset() {
537        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
538        let client_options = ClientOptions {
539            http_headers: Some(http_headers.clone()),
540        };
541        let builder = ClientBuilder::aggregator("", "")
542            .with_options(client_options)
543            .with_origin_tag(None);
544
545        assert_eq!(Some(http_headers), builder.options.http_headers);
546        assert_eq!(None, builder.origin_tag);
547    }
548
549    #[tokio::test]
550    async fn compute_http_headers_with_client_type_returns_options_http_headers_with_client_type() {
551        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
552        let client_builder = ClientBuilder::aggregator("", "")
553            .with_options(ClientOptions {
554                http_headers: Some(http_headers.clone()),
555            })
556            .with_client_type(Some("CLIENT_TYPE".to_string()));
557
558        let computed_headers = client_builder.compute_http_headers();
559
560        assert_eq!(
561            computed_headers,
562            HashMap::from([
563                ("Key".to_string(), "Value".to_string()),
564                (
565                    MITHRIL_CLIENT_TYPE_HEADER.to_string(),
566                    "CLIENT_TYPE".to_string()
567                )
568            ])
569        );
570    }
571
572    #[tokio::test]
573    async fn compute_http_headers_with_options_containing_client_type_returns_client_type() {
574        let http_headers = HashMap::from([(
575            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
576            "client type from options".to_string(),
577        )]);
578        let client_builder = ClientBuilder::aggregator("", "").with_options(ClientOptions {
579            http_headers: Some(http_headers.clone()),
580        });
581
582        let computed_headers = client_builder.compute_http_headers();
583
584        assert_eq!(computed_headers, http_headers);
585    }
586
587    #[tokio::test]
588    async fn test_with_client_type_not_overwrite_other_client_options_attributes() {
589        let builder = ClientBuilder::aggregator("", "")
590            .with_options(ClientOptions { http_headers: None })
591            .with_client_type(Some("TEST".to_string()));
592        assert_eq!(None, builder.options.http_headers);
593        assert_eq!(Some("TEST".to_string()), builder.client_type);
594
595        let http_headers = HashMap::from([("Key".to_string(), "Value".to_string())]);
596        let builder = ClientBuilder::aggregator("", "")
597            .with_options(ClientOptions {
598                http_headers: Some(http_headers.clone()),
599            })
600            .with_client_type(Some("TEST".to_string()));
601        assert_eq!(Some(http_headers), builder.options.http_headers);
602        assert_eq!(Some("TEST".to_string()), builder.client_type);
603    }
604
605    #[tokio::test]
606    async fn test_given_a_none_client_type_compute_http_headers_will_set_client_type_to_default_value()
607     {
608        let builder_without_client_type = ClientBuilder::aggregator("", "");
609        let computed_headers = builder_without_client_type.compute_http_headers();
610
611        assert_eq!(
612            computed_headers,
613            HashMap::from([(
614                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
615                DEFAULT_CLIENT_TYPE.to_string()
616            )])
617        );
618
619        let builder_with_none_client_type =
620            ClientBuilder::aggregator("", "").with_client_type(None);
621        let computed_headers = builder_with_none_client_type.compute_http_headers();
622
623        assert_eq!(
624            computed_headers,
625            HashMap::from([(
626                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
627                DEFAULT_CLIENT_TYPE.to_string()
628            )])
629        );
630    }
631
632    #[tokio::test]
633    async fn test_compute_http_headers_will_compute_client_type_header_from_struct_attribute_over_options()
634     {
635        let http_headers = HashMap::from([(
636            MITHRIL_CLIENT_TYPE_HEADER.to_string(),
637            "client type from options".to_string(),
638        )]);
639        let client_builder = ClientBuilder::aggregator("", "")
640            .with_options(ClientOptions {
641                http_headers: Some(http_headers.clone()),
642            })
643            .with_client_type(Some("client type".to_string()));
644
645        let computed_headers = client_builder.compute_http_headers();
646
647        assert_eq!(
648            computed_headers,
649            HashMap::from([(
650                MITHRIL_CLIENT_TYPE_HEADER.to_string(),
651                "client type".to_string()
652            )])
653        );
654    }
655}