canic_core/ops/ic/
http.rs

1use crate::{
2    Error,
3    // Raw IC HTTP passthrough (infra layer)
4    infra::ic::http::{
5        HttpHeader, HttpMethod, HttpRequestArgs, HttpRequestResult, http_request_raw,
6    },
7    // Observability (ops layer)
8    ops::{adapter::metrics::http::record_http_request, runtime::metrics::record_http_outcall},
9};
10use num_traits::ToPrimitive;
11use serde::de::DeserializeOwned;
12
13///
14/// Http
15/// Approved, observable HTTP helpers over the IC management API.
16///
17
18pub struct Http;
19
20impl Http {
21    /// Maximum allowed response size for HTTP outcalls.
22    pub const MAX_RESPONSE_BYTES: u64 = 200_000;
23
24    //
25    // Internal helpers
26    //
27
28    /// Record outbound HTTP metrics.
29    fn record_metrics(method: HttpMethod, url: &str, label: Option<&str>) {
30        record_http_outcall();
31        record_http_request(method, url, label);
32    }
33
34    //
35    // High-level typed helpers
36    //
37
38    /// Perform an HTTP GET request and deserialize the JSON response.
39    pub async fn get<T: DeserializeOwned>(
40        url: &str,
41        headers: impl AsRef<[(&str, &str)]>,
42    ) -> Result<T, Error> {
43        Self::get_with_label(url, headers, None).await
44    }
45
46    /// Same as `get`, with an optional metrics label.
47    pub async fn get_with_label<T: DeserializeOwned>(
48        url: &str,
49        headers: impl AsRef<[(&str, &str)]>,
50        label: Option<&str>,
51    ) -> Result<T, Error> {
52        // Emit observability signals
53        Self::record_metrics(HttpMethod::GET, url, label);
54
55        // Convert header pairs into IC HTTP headers
56        let headers: Vec<HttpHeader> = headers
57            .as_ref()
58            .iter()
59            .map(|(name, value)| HttpHeader {
60                name: name.to_string(),
61                value: value.to_string(),
62            })
63            .collect();
64
65        // Build raw IC HTTP request arguments
66        let args = HttpRequestArgs {
67            url: url.to_string(),
68            method: HttpMethod::GET,
69            headers,
70            max_response_bytes: Some(Self::MAX_RESPONSE_BYTES),
71            ..Default::default()
72        };
73
74        // Perform raw HTTP outcall via infra
75        let res = http_request_raw(&args)
76            .await
77            .map_err(|e| Error::HttpRequest(e.to_string()))?;
78
79        // Validate HTTP status code
80        let status: u32 = res.status.0.to_u32().unwrap_or(0);
81        if !(200..300).contains(&status) {
82            return Err(Error::HttpErrorCode(status));
83        }
84
85        // Deserialize response body
86        serde_json::from_slice(&res.body).map_err(Into::into)
87    }
88
89    //
90    // Low-level escape hatches
91    //
92
93    /// Perform a raw HTTP request with metrics, returning the IC response verbatim.
94    pub async fn get_raw(args: HttpRequestArgs) -> Result<HttpRequestResult, Error> {
95        Self::get_raw_with_label(args, None).await
96    }
97
98    /// Same as `get_raw`, with an optional metrics label.
99    pub async fn get_raw_with_label(
100        args: HttpRequestArgs,
101        label: Option<&str>,
102    ) -> Result<HttpRequestResult, Error> {
103        // Emit observability signals
104        Self::record_metrics(args.method, &args.url, label);
105
106        // Delegate to infra without additional interpretation
107        http_request_raw(&args)
108            .await
109            .map_err(|e| Error::HttpRequest(e.to_string()))
110    }
111}