1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! An HTTP client implementation to use with the remote-settings-client.

use anyhow::{Context, Error};
use async_trait::async_trait;
use remote_settings_client::client::net::{
    Headers as RsHeaders, Method as RsMethod, Requester as RsRequester, Response as RsResponse,
    Url as RsUrl,
};
use reqwest::{header::CONTENT_TYPE, Method, Response};
use std::time::Duration;

/// An remote-settings-client HTTP client that uses Reqwest.
#[derive(Debug)]
pub struct ReqwestClient {
    /// The client that will be used to make http requests.
    reqwest_client: reqwest::Client,
}

impl ReqwestClient {
    /// Instantiate a new Reqwest client to perform HTTP requests.
    pub fn try_new(
        http_request_timeout: Duration,
        http_connect_timeout: Duration,
    ) -> Result<ReqwestClient, Error> {
        let reqwest_client = reqwest::Client::builder()
            // Disable the connection pool to avoid the IncompleteMessage errors.
            // See #259 for more details.
            .pool_max_idle_per_host(0)
            .connect_timeout(http_connect_timeout)
            .timeout(http_request_timeout)
            .build()?;
        Ok(Self { reqwest_client })
    }
}

#[async_trait]
impl RsRequester for ReqwestClient {
    async fn get(&self, url: RsUrl) -> Result<RsResponse, ()> {
        self.request_json(RsMethod::GET, url, vec![], RsHeaders::default())
            .await
    }

    async fn request_json(
        &self,
        method: RsMethod,
        url: RsUrl,
        data: Vec<u8>,
        headers: RsHeaders,
    ) -> Result<RsResponse, ()> {
        let method = match method {
            RsMethod::GET => Method::GET,
            RsMethod::PATCH => Method::PATCH,
            RsMethod::POST => Method::POST,
            RsMethod::PUT => Method::PUT,
            RsMethod::DELETE => Method::DELETE,
        };
        let headers = (&headers).try_into().map_err(|e| {
            tracing::error!(
                r#type = "adm.remote-settings.reqwest.headers-conversion-failed",
                "ReqwestClient - unable to try_into headers. {:#?}",
                e
            );
        })?;

        match self
            .reqwest_client
            .request(method.clone(), url.clone())
            .header(CONTENT_TYPE, "application/json")
            .headers(headers)
            .body(data)
            .send()
            .await
            .and_then(Response::error_for_status)
            .context(format!(
                "Performing HTTP request for Remote Settings: {}",
                url
            )) {
            Err(e) => {
                tracing::error!(
                    r#type = "adm.remote-settings.reqwest.get-failed",
                    "ReqwestClient - unable to submit {} request. {:#?}",
                    method,
                    e
                );
                Err(())
            }
            Ok(response) => {
                let status = response.status().as_u16();
                let mut headers: RsHeaders = RsHeaders::new();
                for h in response.headers() {
                    headers
                        .entry(h.0.to_string())
                        .or_insert_with(|| h.1.to_str().unwrap_or_default().to_string());
                }

                let body = response.bytes().await.map_err(|err| {
                    tracing::error!(
                        r#type = "adm.remote-settings.reqwest.parsing-failed",
                        "ReqwestClient - unable to parse response body. {:#?}",
                        err
                    );
                })?;

                Ok(RsResponse {
                    status,
                    body: body.to_vec(),
                    headers,
                })
            }
        }
    }
}