ic_cdk/api/management_canister/
http_request.rs1use crate::{
4 api::call::{call_with_payment128, CallResult},
5 id,
6};
7use candid::{
8 parser::types::FuncMode,
9 types::{Function, Serializer, Type},
10 CandidType, Func, Principal,
11};
12use core::hash::Hash;
13use serde::{Deserialize, Serialize};
14#[cfg(feature = "transform-closure")]
15use slotmap::{DefaultKey, Key, SlotMap};
16#[cfg(feature = "transform-closure")]
17use std::cell::RefCell;
18
19#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
21pub struct TransformFunc(pub candid::Func);
22
23impl CandidType for TransformFunc {
24 fn _ty() -> Type {
25 Type::Func(Function {
26 modes: vec![FuncMode::Query],
27 args: vec![TransformArgs::ty()],
28 rets: vec![HttpResponse::ty()],
29 })
30 }
31
32 fn idl_serialize<S: Serializer>(&self, serializer: S) -> Result<(), S::Error> {
33 serializer.serialize_function(self.0.principal.as_slice(), &self.0.method)
34 }
35}
36
37#[derive(CandidType, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub struct TransformArgs {
44 pub response: HttpResponse,
46
47 #[serde(with = "serde_bytes")]
49 pub context: Vec<u8>,
50}
51
52#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
58pub struct TransformContext {
59 pub function: TransformFunc,
61
62 #[serde(with = "serde_bytes")]
64 pub context: Vec<u8>,
65}
66
67impl TransformContext {
68 pub fn from_name(candid_function_name: String, context: Vec<u8>) -> Self {
70 Self {
71 context,
72 function: TransformFunc(Func {
73 method: candid_function_name,
74 principal: id(),
75 }),
76 }
77 }
78}
79
80#[cfg(feature = "transform-closure")]
81thread_local! {
82 #[allow(clippy::type_complexity)]
83 static TRANSFORMS: RefCell<SlotMap<DefaultKey, Box<dyn FnOnce(HttpResponse) -> HttpResponse>>> = RefCell::default();
84}
85
86#[cfg(feature = "transform-closure")]
87#[export_name = "canister_query <ic-cdk internal> http_transform"]
88extern "C" fn http_transform() {
89 use crate::api::{
90 call::{arg_data, reply},
91 caller,
92 };
93 use slotmap::KeyData;
94 if caller() != Principal::management_canister() {
95 crate::trap("This function is internal to ic-cdk and should not be called externally.");
96 }
97 crate::setup();
98 let (args,): (TransformArgs,) = arg_data();
99 let int = u64::from_be_bytes(args.context[..].try_into().unwrap());
100 let key = DefaultKey::from(KeyData::from_ffi(int));
101 let func = TRANSFORMS.with(|transforms| transforms.borrow_mut().remove(key));
102 let Some(func) = func else {
103 crate::trap(&format!("Missing transform function for request {int}"));
104 };
105 let transformed = func(args.response);
106 reply((transformed,))
107}
108
109#[derive(
111 CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default,
112)]
113pub struct HttpHeader {
114 pub name: String,
116 pub value: String,
118}
119
120#[derive(
124 CandidType,
125 Serialize,
126 Deserialize,
127 Debug,
128 PartialEq,
129 Eq,
130 PartialOrd,
131 Ord,
132 Hash,
133 Clone,
134 Copy,
135 Default,
136)]
137pub enum HttpMethod {
138 #[serde(rename = "get")]
140 #[default]
141 GET,
142 #[serde(rename = "post")]
144 POST,
145 #[serde(rename = "head")]
147 HEAD,
148}
149
150#[derive(CandidType, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
152pub struct CanisterHttpRequestArgument {
153 pub url: String,
155 pub max_response_bytes: Option<u64>,
160 pub method: HttpMethod,
162 pub headers: Vec<HttpHeader>,
164 pub body: Option<Vec<u8>>,
166 pub transform: Option<TransformContext>,
169}
170
171#[derive(
173 CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default,
174)]
175pub struct HttpResponse {
176 pub status: candid::Nat,
178 pub headers: Vec<HttpHeader>,
180 pub body: Vec<u8>,
182}
183
184pub async fn http_request(arg: CanisterHttpRequestArgument) -> CallResult<(HttpResponse,)> {
194 let cycles = http_request_required_cycles(&arg);
195 call_with_payment128(
196 Principal::management_canister(),
197 "http_request",
198 (arg,),
199 cycles,
200 )
201 .await
202}
203
204#[cfg(feature = "transform-closure")]
216#[cfg_attr(docsrs, doc(cfg(feature = "transform-closure")))]
217pub async fn http_request_with(
218 arg: CanisterHttpRequestArgument,
219 transform_func: impl FnOnce(HttpResponse) -> HttpResponse + 'static,
220) -> CallResult<(HttpResponse,)> {
221 let cycles = http_request_required_cycles(&arg);
222 http_request_with_cycles_with(arg, cycles, transform_func).await
223}
224
225pub async fn http_request_with_cycles(
234 arg: CanisterHttpRequestArgument,
235 cycles: u128,
236) -> CallResult<(HttpResponse,)> {
237 call_with_payment128(
238 Principal::management_canister(),
239 "http_request",
240 (arg,),
241 cycles,
242 )
243 .await
244}
245
246#[cfg(feature = "transform-closure")]
257#[cfg_attr(docsrs, doc(cfg(feature = "transform-closure")))]
258pub async fn http_request_with_cycles_with(
259 arg: CanisterHttpRequestArgument,
260 cycles: u128,
261 transform_func: impl FnOnce(HttpResponse) -> HttpResponse + 'static,
262) -> CallResult<(HttpResponse,)> {
263 assert!(
264 arg.transform.is_none(),
265 "`CanisterHttpRequestArgument`'s `transform` field must be `None` when using a closure"
266 );
267 let transform_func = Box::new(transform_func) as _;
268 let key = TRANSFORMS.with(|transforms| transforms.borrow_mut().insert(transform_func));
269 struct DropGuard(DefaultKey);
270 impl Drop for DropGuard {
271 fn drop(&mut self) {
272 TRANSFORMS.with(|transforms| transforms.borrow_mut().remove(self.0));
273 }
274 }
275 let key = DropGuard(key);
276 let context = key.0.data().as_ffi().to_be_bytes().to_vec();
277 let arg = CanisterHttpRequestArgument {
278 transform: Some(TransformContext {
279 function: TransformFunc(candid::Func {
280 method: "<ic-cdk internal> http_transform".into(),
281 principal: crate::id(),
282 }),
283 context,
284 }),
285 ..arg
286 };
287 http_request_with_cycles(arg, cycles).await
288}
289
290fn http_request_required_cycles(arg: &CanisterHttpRequestArgument) -> u128 {
291 let max_response_bytes = match arg.max_response_bytes {
292 Some(ref n) => *n as u128,
293 None => 2 * 1024 * 1024u128, };
295 let arg_raw = candid::utils::encode_args((arg,)).expect("Failed to encode arguments.");
296 400_000_000u128 + 100_000u128 * (arg_raw.len() as u128 + 12 + max_response_bytes)
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn required_cycles_some_max() {
307 let url = "https://example.com".to_string();
308 let arg = CanisterHttpRequestArgument {
309 url,
310 max_response_bytes: Some(3000),
311 method: HttpMethod::GET,
312 headers: vec![],
313 body: None,
314 transform: None,
315 };
316 assert_eq!(http_request_required_cycles(&arg), 718500000u128);
317 }
318
319 #[test]
320 fn required_cycles_none_max() {
321 let url = "https://example.com".to_string();
322 let arg = CanisterHttpRequestArgument {
323 url,
324 max_response_bytes: None,
325 method: HttpMethod::GET,
326 headers: vec![],
327 body: None,
328 transform: None,
329 };
330 assert_eq!(http_request_required_cycles(&arg), 210132900000u128);
331 }
332}