From d109675a11292d64a7d19329ecc9e3738ca33729 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Sat, 4 Apr 2026 09:23:37 +0200 Subject: [PATCH 1/2] docs: review HTTPs outcalls in Rust Signed-off-by: David Dal Busco --- docs/guides/rust.mdx | 67 ++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/docs/guides/rust.mdx b/docs/guides/rust.mdx index 1406a195..3b168931 100644 --- a/docs/guides/rust.mdx +++ b/docs/guides/rust.mdx @@ -241,9 +241,9 @@ To encode and decode the call, you need Rust structures that match the Candid ty --- -## HTTPS outcalls +## HTTPS Outcalls -[HTTPS outcalls](https://internetcomputer.org/https-outcalls) are a feature of the Internet Computer, enabling smart contracts to directly connect to the Web 2.0 world by querying APIs with HTTP requests. +[HTTPS outcalls](https://internetcomputer.org/https-outcalls) are a feature that enables your serverless functions to make HTTP requests to any external API. :::tip @@ -260,18 +260,10 @@ For this example, we'll skip a few steps as the logic remains consistent: Here is an example of an `on_set_doc` hook which fetches an API to get the link to an image of a dog and saves that information within the Datastore. While this might not be a practical real-world use case, it is simple enough to demonstrate the feature. ```rust -use ic_cdk::api::management_canister::http_request::{ - http_request as http_request_outcall, CanisterHttpRequestArgument, HttpMethod, -}; -use junobuild_macros::{ - on_delete_asset, on_delete_doc, on_delete_many_assets, on_delete_many_docs, on_set_doc, - on_set_many_docs, on_upload_asset, -}; -use junobuild_satellite::{ - include_satellite, set_doc_store, OnDeleteAssetContext, OnDeleteDocContext, - OnDeleteManyAssetsContext, OnDeleteManyDocsContext, OnSetDocContext, OnSetManyDocsContext, - OnUploadAssetContext, SetDoc, -}; +use ic_cdk::management_canister::http_request as http_request_outcall; +use ic_cdk::management_canister::{HttpMethod, HttpRequestArgs}; +use junobuild_macros::on_set_doc; +use junobuild_satellite::{include_satellite, set_doc_store, OnSetDocContext, SetDoc}; use junobuild_utils::encode_doc_data; use serde::{Deserialize, Serialize}; @@ -304,7 +296,7 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { let request_headers = vec![]; - let request = CanisterHttpRequestArgument { + let request = HttpRequestArgs { url, method: HttpMethod::GET, body: None, @@ -313,18 +305,14 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { transform: None, // We do not require any particular HTTP headers in this example. headers: request_headers, + // Use a single node as we do not require that a trust level for fetching a dog image for demo purposes. 😉 + is_replicated: Some(false), }; - // 2. Execute the HTTP request. A request consumes Cycles(!). In this example we provide 2_000_000_000 Cycles (= 0.002 TCycles). - // To estimate the costs see documentation: - // - https://internetcomputer.org/docs/current/developer-docs/gas-cost#special-features - // - https://internetcomputer.org/docs/current/developer-docs/integrations/https-outcalls/https-outcalls-how-it-works#pricing - // Total amount of cycles depends on the subnet size. Therefore, on mainnet it might cost ~13x more than what's required when developing locally. Source: https://forum.dfinity.org/t/http-outcalls-cycles/27439/4 - // Note: In the future we will have a UI logging panel in console.juno.build to help debug on production. Follow PR https://github.com/junobuild/juno/issues/415. - // - // We rename ic_cdk::api::management_canister::http_request::http_request to http_request_outcall because the Satellite already includes such a function's name. - match http_request_outcall(request, 2_000_000_000).await { - Ok((response,)) => { + // 2. Execute the HTTP request. + // Note: we alias the function http_request to http_request_outcall just to prevent naming conflicts. + match http_request_outcall(&request).await { + Ok(response) => { // 3. Use serde_json to transform the response to a structured object. let str_body = String::from_utf8(response.body) .expect("Transformed response is not UTF-8 encoded."); @@ -357,43 +345,36 @@ async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { Ok(()) } - Err((r, m)) => { - let message = - format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}"); + Err(error) => { + let message = format!("The http_request resulted into error: {error:?}"); Err(message) } } } -// Other hooks - include_satellite!(); ``` As with the previous example, the hook will asynchronously update the document. If you wait a bit before retrieving the document in your frontend, you might notice that the source of the image has been updated by your hook. -### Costs +### Replication -HTTPS outcalls require cycles to execute the request. At the time of writing this example, the cost was calculated using the formula `(3_000_000 + 60_000 * n) * n` for the base fee and `400 * n` each request byte and `800 * n` for each response byte, where n is the number of nodes in the subnet. +By default, all nodes that run your Satellite execute the same request and must agree on the response for the call to succeed. This ensures the result is verified but means the target API must return identical responses across repeated calls. -You can use the [HTTPS Outcalls Cost Calculator](https://7joko-hiaaa-aaaal-ajz7a-cai.icp0.io/) to estimate the cost of your request (source code is available on [GitHub](https://github.com/domwoe/HTTPS-Outcalls-Calculator)). +Setting `is_replicated: Some(false)` switches to a single-node mode which by extension skips such assertion. It's also cheaper, but the response is not verified by others. Suitable when you trust the data source or consistency is not critical. -Alternatively, refer to the [documentation](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works#pricing) for the actual calculation method and costs. - -### Technical Requirements +### Costs -The goal of HTTPS outcalls is to ensure that a request to the Web2 world returns a valid and verifiable response. To achieve this, calls are replicated when executed on mainnet. This means the blockchain will perform multiple identical requests and compare their results. The response will only succeed if all returned results are exactly the same. +HTTPS outcalls consume cycles to execute. Refer to the [ICP documentation](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works#pricing) for the current pricing model. -Many Web APIs do not natively support such replicated calls. More advanced APIs offer a way to handle this by using an **idempotency key**, a unique key that allows the server to recognize and return the same response for repeated requests. +You can also use the [HTTPS Outcalls Cost Calculator](https://7joko-hiaaa-aaaal-ajz7a-cai.icp0.io/) to estimate the cost of a specific request. -Another requirement is that the API must be accessible over **IPv6**. +### Technical Requirements -If replication or IPv6 support is not available, a common workaround is to use a proxy service. Developers can consider the following solutions: +HTTPS outcalls support both IPv4 and IPv6. -- [`ic-http-proxy`](https://github.com/omnia-network/ic-http-proxy) from Omnia Network ([announcement](https://forum.dfinity.org/t/non-replicated-https-outcalls/26627)) -- [`idempotent-proxy`](https://github.com/ldclabs/idempotent-proxy) from LDC Labs ([announcement](https://forum.dfinity.org/t/idempotent-proxy-show-proxy-https-outcalls-to-any-web2-service/32754)) -- A similar approach to the optimistic solution we currently used for transmitting emails ([repo](https://github.com/junobuild/proxy)) +One consideration when using replicated mode: since all nodes execute the same request, the API must return an identical response each time. Many APIs support this via an idempotency key. If that's not an option, non-replicated mode is usually the practical alternative. --- From 66b40380b0311fec0e050689b191dde22a9c2cb3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 07:25:13 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=84=20Update=20LLMs.txt=20snapshot?= =?UTF-8?q?=20for=20PR=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .llms-snapshots/llms-full.txt | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/.llms-snapshots/llms-full.txt b/.llms-snapshots/llms-full.txt index 72bd033a..5968ef8e 100644 --- a/.llms-snapshots/llms-full.txt +++ b/.llms-snapshots/llms-full.txt @@ -7171,9 +7171,9 @@ To encode and decode the call, you need Rust structures that match the Candid ty --- -## HTTPS outcalls +## HTTPS Outcalls -[HTTPS outcalls](https://internetcomputer.org/https-outcalls) are a feature of the Internet Computer, enabling smart contracts to directly connect to the Web 2.0 world by querying APIs with HTTP requests. +[HTTPS outcalls](https://internetcomputer.org/https-outcalls) are a feature that enables your serverless functions to make HTTP requests to any external API. **Tip:** @@ -7188,32 +7188,28 @@ For this example, we'll skip a few steps as the logic remains consistent: Here is an example of an `on_set_doc` hook which fetches an API to get the link to an image of a dog and saves that information within the Datastore. While this might not be a practical real-world use case, it is simple enough to demonstrate the feature. ``` -use ic_cdk::api::management_canister::http_request::{ http_request as http_request_outcall, CanisterHttpRequestArgument, HttpMethod,};use junobuild_macros::{ on_delete_asset, on_delete_doc, on_delete_many_assets, on_delete_many_docs, on_set_doc, on_set_many_docs, on_upload_asset,};use junobuild_satellite::{ include_satellite, set_doc_store, OnDeleteAssetContext, OnDeleteDocContext, OnDeleteManyAssetsContext, OnDeleteManyDocsContext, OnSetDocContext, OnSetManyDocsContext, OnUploadAssetContext, SetDoc,};use junobuild_utils::encode_doc_data;use serde::{Deserialize, Serialize};// The data of the document we are looking to update in the Satellite's Datastore.#[derive(Serialize, Deserialize)]struct DogData { src: Option,}// We are using the Dog CEO API in this example.// https://dog.ceo/dog-api///// Its endpoint "random" returns such JSON data:// {// "message": "https://images.dog.ceo/breeds/mountain-swiss/n02107574_1118.jpg",// "status": "success"// }//// That's why we declare a struct that matches the structure of the answer.#[derive(Serialize, Deserialize)]struct DogApiResponse { message: String, status: String,}#[on_set_doc(collections = ["dogs"])]async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { // 1. Prepare the HTTP GET request let url = "https://dog.ceo/api/breeds/image/random".to_string(); let request_headers = vec![]; let request = CanisterHttpRequestArgument { url, method: HttpMethod::GET, body: None, max_response_bytes: None, // In this simple example we skip sanitizing the response with a custom function for simplicity reason. transform: None, // We do not require any particular HTTP headers in this example. headers: request_headers, }; // 2. Execute the HTTP request. A request consumes Cycles(!). In this example we provide 2_000_000_000 Cycles (= 0.002 TCycles). // To estimate the costs see documentation: // - https://internetcomputer.org/docs/current/developer-docs/gas-cost#special-features // - https://internetcomputer.org/docs/current/developer-docs/integrations/https-outcalls/https-outcalls-how-it-works#pricing // Total amount of cycles depends on the subnet size. Therefore, on mainnet it might cost ~13x more than what's required when developing locally. Source: https://forum.dfinity.org/t/http-outcalls-cycles/27439/4 // Note: In the future we will have a UI logging panel in console.juno.build to help debug on production. Follow PR https://github.com/junobuild/juno/issues/415. // // We rename ic_cdk::api::management_canister::http_request::http_request to http_request_outcall because the Satellite already includes such a function's name. match http_request_outcall(request, 2_000_000_000).await { Ok((response,)) => { // 3. Use serde_json to transform the response to a structured object. let str_body = String::from_utf8(response.body) .expect("Transformed response is not UTF-8 encoded."); let dog_response: DogApiResponse = serde_json::from_str(&str_body).map_err(|e| e.to_string())?; // 4. Our goal is to update the document in the Datastore with an update that contains the link to the image fetched from the API we just called. let dog: DogData = DogData { src: Some(dog_response.message), }; // 5. We encode those data back to blob because the Datastore holds data as blob. let encode_data = encode_doc_data(&dog)?; // 6. Then we construct the parameters required to call the function that save the data in the Datastore. let doc: SetDoc = SetDoc { data: encode_data, description: context.data.data.after.description, version: context.data.data.after.version, }; // 7. We store the data in the Datastore for the same caller as the one who triggered the original on_set_doc, in the same collection with the same key as well. set_doc_store( context.caller, context.data.collection, context.data.key, doc, )?; Ok(()) } Err((r, m)) => { let message = format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}"); Err(message) } }}// Other hooksinclude_satellite!(); +use ic_cdk::management_canister::http_request as http_request_outcall;use ic_cdk::management_canister::{HttpMethod, HttpRequestArgs};use junobuild_macros::on_set_doc;use junobuild_satellite::{include_satellite, set_doc_store, OnSetDocContext, SetDoc};use junobuild_utils::encode_doc_data;use serde::{Deserialize, Serialize};// The data of the document we are looking to update in the Satellite's Datastore.#[derive(Serialize, Deserialize)]struct DogData { src: Option,}// We are using the Dog CEO API in this example.// https://dog.ceo/dog-api///// Its endpoint "random" returns such JSON data:// {// "message": "https://images.dog.ceo/breeds/mountain-swiss/n02107574_1118.jpg",// "status": "success"// }//// That's why we declare a struct that matches the structure of the answer.#[derive(Serialize, Deserialize)]struct DogApiResponse { message: String, status: String,}#[on_set_doc(collections = ["dogs"])]async fn on_set_doc(context: OnSetDocContext) -> Result<(), String> { // 1. Prepare the HTTP GET request let url = "https://dog.ceo/api/breeds/image/random".to_string(); let request_headers = vec![]; let request = HttpRequestArgs { url, method: HttpMethod::GET, body: None, max_response_bytes: None, // In this simple example we skip sanitizing the response with a custom function for simplicity reason. transform: None, // We do not require any particular HTTP headers in this example. headers: request_headers, // Use a single node as we do not require that a trust level for fetching a dog image for demo purposes. 😉 is_replicated: Some(false), }; // 2. Execute the HTTP request. // Note: we alias the function http_request to http_request_outcall just to prevent naming conflicts. match http_request_outcall(&request).await { Ok(response) => { // 3. Use serde_json to transform the response to a structured object. let str_body = String::from_utf8(response.body) .expect("Transformed response is not UTF-8 encoded."); let dog_response: DogApiResponse = serde_json::from_str(&str_body).map_err(|e| e.to_string())?; // 4. Our goal is to update the document in the Datastore with an update that contains the link to the image fetched from the API we just called. let dog: DogData = DogData { src: Some(dog_response.message), }; // 5. We encode those data back to blob because the Datastore holds data as blob. let encode_data = encode_doc_data(&dog)?; // 6. Then we construct the parameters required to call the function that save the data in the Datastore. let doc: SetDoc = SetDoc { data: encode_data, description: context.data.data.after.description, version: context.data.data.after.version, }; // 7. We store the data in the Datastore for the same caller as the one who triggered the original on_set_doc, in the same collection with the same key as well. set_doc_store( context.caller, context.data.collection, context.data.key, doc, )?; Ok(()) } Err(error) => { let message = format!("The http_request resulted into error: {error:?}"); Err(message) } }}include_satellite!(); ``` As with the previous example, the hook will asynchronously update the document. If you wait a bit before retrieving the document in your frontend, you might notice that the source of the image has been updated by your hook. -### Costs - -HTTPS outcalls require cycles to execute the request. At the time of writing this example, the cost was calculated using the formula `(3_000_000 + 60_000 * n) * n` for the base fee and `400 * n` each request byte and `800 * n` for each response byte, where n is the number of nodes in the subnet. +### Replication -You can use the [HTTPS Outcalls Cost Calculator](https://7joko-hiaaa-aaaal-ajz7a-cai.icp0.io/) to estimate the cost of your request (source code is available on [GitHub](https://github.com/domwoe/HTTPS-Outcalls-Calculator)). +By default, all nodes that run your Satellite execute the same request and must agree on the response for the call to succeed. This ensures the result is verified but means the target API must return identical responses across repeated calls. -Alternatively, refer to the [documentation](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works#pricing) for the actual calculation method and costs. +Setting `is_replicated: Some(false)` switches to a single-node mode which by extension skips such assertion. It's also cheaper, but the response is not verified by others. Suitable when you trust the data source or consistency is not critical. -### Technical Requirements +### Costs -The goal of HTTPS outcalls is to ensure that a request to the Web2 world returns a valid and verifiable response. To achieve this, calls are replicated when executed on mainnet. This means the blockchain will perform multiple identical requests and compare their results. The response will only succeed if all returned results are exactly the same. +HTTPS outcalls consume cycles to execute. Refer to the [ICP documentation](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works#pricing) for the current pricing model. -Many Web APIs do not natively support such replicated calls. More advanced APIs offer a way to handle this by using an **idempotency key**, a unique key that allows the server to recognize and return the same response for repeated requests. +You can also use the [HTTPS Outcalls Cost Calculator](https://7joko-hiaaa-aaaal-ajz7a-cai.icp0.io/) to estimate the cost of a specific request. -Another requirement is that the API must be accessible over **IPv6**. +### Technical Requirements -If replication or IPv6 support is not available, a common workaround is to use a proxy service. Developers can consider the following solutions: +HTTPS outcalls support both IPv4 and IPv6. -* [`ic-http-proxy`](https://github.com/omnia-network/ic-http-proxy) from Omnia Network ([announcement](https://forum.dfinity.org/t/non-replicated-https-outcalls/26627)) -* [`idempotent-proxy`](https://github.com/ldclabs/idempotent-proxy) from LDC Labs ([announcement](https://forum.dfinity.org/t/idempotent-proxy-show-proxy-https-outcalls-to-any-web2-service/32754)) -* A similar approach to the optimistic solution we currently used for transmitting emails ([repo](https://github.com/junobuild/proxy)) +One consideration when using replicated mode: since all nodes execute the same request, the API must return an identical response each time. Many APIs support this via an idempotency key. If that's not an option, non-replicated mode is usually the practical alternative. ---