ic_cdk/api/management_canister/
http_request.rs

1//! Canister HTTP request.
2
3use 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/// "transform" function of type: `func (http_request) -> (http_response) query`
20#[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/// Type used for encoding/decoding:
38/// `record {
39///     response : http_response;
40///     context : blob;
41/// }`
42#[derive(CandidType, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub struct TransformArgs {
44    /// Raw response from remote service, to be transformed
45    pub response: HttpResponse,
46
47    /// Context for response transformation
48    #[serde(with = "serde_bytes")]
49    pub context: Vec<u8>,
50}
51
52/// Type used for encoding/decoding:
53/// `record {
54///     function : func (record {response : http_response; context : blob}) -> (http_response) query;
55///     context : blob;
56/// }`
57#[derive(CandidType, Clone, Debug, Deserialize, PartialEq, Eq)]
58pub struct TransformContext {
59    /// Reference function with signature: `func (record {response : http_response; context : blob}) -> (http_response) query;`.
60    pub function: TransformFunc,
61
62    /// Context to be passed to `transform` function to transform HTTP response for consensus
63    #[serde(with = "serde_bytes")]
64    pub context: Vec<u8>,
65}
66
67impl TransformContext {
68    /// Constructs a TransformContext from a name and context. The principal is assumed to be the [current canister's](id).
69    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/// HTTP header.
110#[derive(
111    CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default,
112)]
113pub struct HttpHeader {
114    /// Name
115    pub name: String,
116    /// Value
117    pub value: String,
118}
119
120/// HTTP method.
121///
122/// Currently support following methods.
123#[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    /// GET
139    #[serde(rename = "get")]
140    #[default]
141    GET,
142    /// POST
143    #[serde(rename = "post")]
144    POST,
145    /// HEAD
146    #[serde(rename = "head")]
147    HEAD,
148}
149
150/// Argument type of [http_request].
151#[derive(CandidType, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
152pub struct CanisterHttpRequestArgument {
153    /// The requested URL.
154    pub url: String,
155    /// The maximal size of the response in bytes. If None, 2MiB will be the limit.
156    /// This value affects the cost of the http request and it is highly recommended
157    /// to set it as low as possible to avoid unnecessary extra costs.
158    /// See also the [pricing section of HTTP outcalls documentation](https://internetcomputer.org/docs/current/developer-docs/integrations/http_requests/http_requests-how-it-works#pricing).
159    pub max_response_bytes: Option<u64>,
160    /// The method of HTTP request.
161    pub method: HttpMethod,
162    /// List of HTTP request headers and their corresponding values.
163    pub headers: Vec<HttpHeader>,
164    /// Optionally provide request body.
165    pub body: Option<Vec<u8>>,
166    /// Name of the transform function which is `func (transform_args) -> (http_response) query`.
167    /// Set to `None` if you are using `http_request_with` or `http_request_with_cycles_with`.
168    pub transform: Option<TransformContext>,
169}
170
171/// The returned HTTP response.
172#[derive(
173    CandidType, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Default,
174)]
175pub struct HttpResponse {
176    /// The response status (e.g., 200, 404).
177    pub status: candid::Nat,
178    /// List of HTTP response headers and their corresponding values.
179    pub headers: Vec<HttpHeader>,
180    /// The response’s body.
181    pub body: Vec<u8>,
182}
183
184/// Make an HTTP request to a given URL and return the HTTP response, possibly after a transformation.
185///
186/// See [IC method `http_request`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-http_request).
187///
188/// This call requires cycles payment. The required cycles is a function of the request size and max_response_bytes.
189/// This method handles the cycles cost calculation under the hood which assuming the canister is on a 13-node Application Subnet.
190/// If the canister is on a 34-node Application Subnets, you may have to compute the cost by yourself and call [`http_request_with_cycles`] instead.
191///
192/// Check [this page](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs) for more details.
193pub 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/// Make an HTTP request to a given URL and return the HTTP response, after a transformation.
205///
206/// Do not set the `transform` field of `arg`. To use a Candid function, call [`http_request`] instead.
207///
208/// See [IC method `http_request`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-http_request).
209///
210/// This call requires cycles payment. The required cycles is a function of the request size and max_response_bytes.
211/// This method handles the cycles cost calculation under the hood which assuming the canister is on a 13-node Application Subnet.
212/// If the canister is on a 34-node Application Subnets, you may have to compute the cost by yourself and call [`http_request_with_cycles_with`] instead.
213///
214/// Check [this page](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs) for more details.
215#[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
225/// Make an HTTP request to a given URL and return the HTTP response, possibly after a transformation.
226///
227/// See [IC method `http_request`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-http_request).
228///
229/// This call requires cycles payment. The required cycles is a function of the request size and max_response_bytes.
230/// Check [this page](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs) for more details.
231///
232/// If the canister is on a 13-node Application Subnet, you can call [`http_request`] instead which handles cycles cost calculation under the hood.
233pub 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/// Make an HTTP request to a given URL and return the HTTP response, after a transformation.
247///
248/// Do not set the `transform` field of `arg`. To use a Candid function, call [`http_request_with_cycles`] instead.
249///
250/// See [IC method `http_request`](https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-http_request).
251///
252/// This call requires cycles payment. The required cycles is a function of the request size and max_response_bytes.
253/// Check [this page](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs) for more details.
254///
255/// If the canister is on a 13-node Application Subnet, you can call [`http_request_with`] instead which handles cycles cost calculation under the hood.
256#[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, // default 2MiB
294    };
295    let arg_raw = candid::utils::encode_args((arg,)).expect("Failed to encode arguments.");
296    // The coefficients can be found in [this page](https://internetcomputer.org/docs/current/developer-docs/production/computation-and-storage-costs).
297    // 12 is "http_request".len().
298    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}