Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 12 additions & 16 deletions .llms-snapshots/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand All @@ -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<String>,}// 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<String>,}// 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.

---

Expand Down
67 changes: 24 additions & 43 deletions docs/guides/rust.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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};

Expand Down Expand Up @@ -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,
Expand All @@ -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.");
Expand Down Expand Up @@ -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.

---

Expand Down