contrag_core/embedders/
http_client.rs

1use serde::{Deserialize, Serialize};
2use crate::error::{ContragError, Result};
3
4/// HTTP client for making outcalls from ICP canisters
5/// 
6/// This wraps the ICP HTTP outcall functionality for easier use.
7pub struct HttpClient {
8    max_response_bytes: u64,
9}
10
11impl HttpClient {
12    pub fn new() -> Self {
13        Self {
14            max_response_bytes: 2_000_000, // 2MB default
15        }
16    }
17
18    /// Make an HTTP POST request
19    /// 
20    /// In WASM/canister environment, this uses ic_cdk::api::management_canister::http_request
21    /// In non-WASM (tests), this returns an error
22    pub async fn post(
23        &self,
24        url: String,
25        headers: Vec<(String, String)>,
26        body: Vec<u8>,
27    ) -> Result<HttpOutcallResponse> {
28        #[cfg(target_family = "wasm")]
29        {
30            use ic_cdk::api::management_canister::http_request::{
31                http_request, CanisterHttpRequestArgument, HttpMethod, HttpHeader, HttpResponse,
32                TransformContext,
33            };
34
35            let request_headers: Vec<HttpHeader> = headers
36                .into_iter()
37                .map(|(name, value)| HttpHeader { name, value })
38                .collect();
39
40            let request = CanisterHttpRequestArgument {
41                url,
42                method: HttpMethod::POST,
43                body: Some(body),
44                max_response_bytes: Some(self.max_response_bytes),
45                transform: None,
46                headers: request_headers,
47            };
48
49            let cycles = 1_000_000_000u128; // 1B cycles
50
51            match http_request(request, cycles).await {
52                Ok((response,)) => Ok(HttpOutcallResponse {
53                    status: response.status.0.into(),
54                    headers: response
55                        .headers
56                        .into_iter()
57                        .map(|h| (h.name, h.value))
58                        .collect(),
59                    body: response.body,
60                }),
61                Err((code, msg)) => Err(ContragError::HttpOutcallError(format!(
62                    "HTTP outcall failed: {:?} - {}",
63                    code, msg
64                ))),
65            }
66        }
67
68        #[cfg(not(target_family = "wasm"))]
69        {
70            Err(ContragError::HttpOutcallError(
71                "HTTP outcalls only work in WASM environment".to_string(),
72            ))
73        }
74    }
75
76    /// Make an HTTP GET request
77    pub async fn get(
78        &self,
79        url: String,
80        headers: Vec<(String, String)>,
81    ) -> Result<HttpOutcallResponse> {
82        #[cfg(target_family = "wasm")]
83        {
84            use ic_cdk::api::management_canister::http_request::{
85                http_request, CanisterHttpRequestArgument, HttpMethod, HttpHeader,
86                TransformContext,
87            };
88
89            let request_headers: Vec<HttpHeader> = headers
90                .into_iter()
91                .map(|(name, value)| HttpHeader { name, value })
92                .collect();
93
94            let request = CanisterHttpRequestArgument {
95                url,
96                method: HttpMethod::GET,
97                body: None,
98                max_response_bytes: Some(self.max_response_bytes),
99                transform: None,
100                headers: request_headers,
101            };
102
103            let cycles = 500_000_000u128; // 500M cycles
104
105            match http_request(request, cycles).await {
106                Ok((response,)) => Ok(HttpOutcallResponse {
107                    status: response.status.0.into(),
108                    headers: response
109                        .headers
110                        .into_iter()
111                        .map(|h| (h.name, h.value))
112                        .collect(),
113                    body: response.body,
114                }),
115                Err((code, msg)) => Err(ContragError::HttpOutcallError(format!(
116                    "HTTP outcall failed: {:?} - {}",
117                    code, msg
118                ))),
119            }
120        }
121
122        #[cfg(not(target_family = "wasm"))]
123        {
124            Err(ContragError::HttpOutcallError(
125                "HTTP outcalls only work in WASM environment".to_string(),
126            ))
127        }
128    }
129}
130
131impl Default for HttpClient {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct HttpOutcallResponse {
139    pub status: u16,
140    pub headers: Vec<(String, String)>,
141    pub body: Vec<u8>,
142}
143
144impl HttpOutcallResponse {
145    /// Parse body as JSON
146    pub fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
147        serde_json::from_slice(&self.body).map_err(|e| {
148            ContragError::SerializationError(format!("Failed to parse JSON response: {}", e))
149        })
150    }
151
152    /// Get body as string
153    pub fn text(&self) -> Result<String> {
154        String::from_utf8(self.body.clone()).map_err(|e| {
155            ContragError::SerializationError(format!("Failed to parse response as UTF-8: {}", e))
156        })
157    }
158}