How to use HTTPS outcalls: GET
A minimal example to make a GET
HTTP request. The purpose of this dapp is only to show how to make HTTP requests from a canister. It sends a GET
request to the Coinbase API and retrieves some historical data about the ICP token.
GET
example
- Motoko
- Rust
import Blob "mo:base/Blob";
import Cycles "mo:base/ExperimentalCycles";
import Nat64 "mo:base/Nat64";
import Text "mo:base/Text";
import IC "ic:aaaaa-aa";
//Actor
actor {
//This method sends a GET request to a URL with a free API we can test.
//This method returns Coinbase data on the exchange rate between USD and ICP
//for a certain day.
//The API response looks like this:
// [
// [
// 1682978460, <-- start timestamp
// 5.714, <-- lowest price during time range
// 5.718, <-- highest price during range
// 5.714, <-- price at open
// 5.714, <-- price at close
// 243.5678 <-- volume of ICP traded
// ],
// ]
//function to transform the response
public query func transform({
context : Blob;
response : IC.http_request_result;
}) : async IC.http_request_result {
{
response with headers = []; // not intersted in the headers
};
};
public func get_icp_usd_exchange() : async Text {
//1. SETUP ARGUMENTS FOR HTTP GET request
let ONE_MINUTE : Nat64 = 60;
let start_timestamp : Nat64 = 1682978460; //May 1, 2023 22:01:00 GMT
let host : Text = "api.exchange.coinbase.com";
let url = "https://" # host # "/products/ICP-USD/candles?start=" # Nat64.toText(start_timestamp) # "&end=" # Nat64.toText(start_timestamp) # "&granularity=" # Nat64.toText(ONE_MINUTE);
// 1.2 prepare headers for the system http_request call
let request_headers = [
{ name = "User-Agent"; value = "price-feed" },
];
// 1.3 The HTTP request
let http_request : IC.http_request_args = {
url = url;
max_response_bytes = null; //optional for request
headers = request_headers;
body = null; //optional for request
method = #get;
transform = ?{
function = transform;
context = Blob.fromArray([]);
};
};
//2. ADD CYCLES TO PAY FOR HTTP REQUEST
//IC management canister will make the HTTP request so it needs cycles
//See: https://internetcomputer.org/docs/current/motoko/main/cycles
//The way Cycles.add() works is that it adds those cycles to the next asynchronous call
//See:
// - https://internetcomputer.org/docs/current/references/ic-interface-spec/#ic-http_request
// - https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works#pricing
// - https://internetcomputer.org/docs/current/developer-docs/gas-cost
Cycles.add<system>(230_949_972_000);
//3. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE
let http_response : IC.http_request_result = await IC.http_request(http_request);
//4. DECODE THE RESPONSE
//As per the type declarations, the BODY in the HTTP response
//comes back as Blob. Type signature:
//public type http_request_result = {
// status : Nat;
// headers : [HttpHeader];
// body : Blob;
// };
//We need to decode that Blob that is the body into readable text.
//To do this, we:
// 1. Use Text.decodeUtf8() method to convert the Blob to a ?Text optional
// 2. We use a switch to explicitly call out both cases of decoding the Blob into ?Text
let decoded_text : Text = switch (Text.decodeUtf8(http_response.body)) {
case (null) { "No value returned" };
case (?y) { y };
};
//5. RETURN RESPONSE OF THE BODY
//The API response will looks like this:
//
// ("[[1682978460,5.714,5.718,5.714,5.714,243.5678]]")
//
//The API response looks like this:
// [
// [
// 1682978460, <-- start timestamp
// 5.714, <-- lowest price during time range
// 5.718, <-- highest price during range
// 5.714, <-- price at open
// 5.714, <-- price at close
// 243.5678 <-- volume of ICP traded
// ],
// ]
decoded_text;
};
};
get_icp_usd_exchange()
is an update call. All methods that make HTTPS outcalls must be update calls because they go through consensus, even if the HTTPS outcall is aGET
.- The code above adds
20_949_972_000
cycles. This is typically enough forGET
requests, but this may need to change depending on your use case. - The code above imports
Types.mo
to separate the custom types from the actor file (as a best practice).
//1. IMPORT IC MANAGEMENT CANISTER
//This includes all methods and types needed
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs,
TransformContext,
};
use ic_cdk_macros::{self, query, update};
use serde::{Serialize, Deserialize};
use serde_json::{self, Value};
// This struct is legacy code and is not really used in the code.
#[derive(Serialize, Deserialize)]
struct Context {
bucket_start_time_index: usize,
closing_price_index: usize,
}
//Update method using the HTTPS outcalls feature
#[ic_cdk::update]
async fn get_icp_usd_exchange() -> String {
//2. SETUP ARGUMENTS FOR HTTP GET request
// 2.1 Setup the URL and its query parameters
type Timestamp = u64;
let start_timestamp: Timestamp = 1682978460; //May 1, 2023 22:01:00 GMT
let seconds_of_time: u64 = 60; //we start with 60 seconds
let host = "api.exchange.coinbase.com";
let url = format!(
"https://{}/products/ICP-USD/candles?start={}&end={}&granularity={}",
host,
start_timestamp.to_string(),
start_timestamp.to_string(),
seconds_of_time.to_string()
);
// 2.2 prepare headers for the system http_request call
//Note that `HttpHeader` is declared in line 4
let request_headers = vec![
HttpHeader {
name: "Host".to_string(),
value: format!("{host}:443"),
},
HttpHeader {
name: "User-Agent".to_string(),
value: "exchange_rate_canister".to_string(),
},
];
// This struct is legacy code and is not really used in the code. Need to be removed in the future
// The "TransformContext" function does need a CONTEXT parameter, but this implementation is not necessary
// the TransformContext(transform, context) below accepts this "context", but it does nothing with it in this implementation.
// bucket_start_time_index and closing_price_index are meaninglesss
let context = Context {
bucket_start_time_index: 0,
closing_price_index: 4,
};
//note "CanisterHttpRequestArgument" and "HttpMethod" are declared in line 4
let request = CanisterHttpRequestArgument {
url: url.to_string(),
method: HttpMethod::GET,
body: None, //optional for request
max_response_bytes: None, //optional for request
// transform: None, //optional for request
transform: Some(TransformContext::new(transform, serde_json::to_vec(&context).unwrap())),
headers: request_headers,
};
//3. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE
//Note: in Rust, `http_request()` already sends the cycles needed
//so no need for explicit Cycles.add() as in Motoko
match http_request(request).await {
//4. DECODE AND RETURN THE RESPONSE
//See:https://docs.rs/ic-cdk/latest/ic_cdk/api/management_canister/http_request/struct.HttpResponse.html
Ok((response,)) => {
//if successful, `HttpResponse` has this structure:
// pub struct HttpResponse {
// pub status: Nat,
// pub headers: Vec<HttpHeader>,
// pub body: Vec<u8>,
// }
//We need to decode that Vec<u8> that is the body into readable text.
//To do this, we:
// 1. Call `String::from_utf8()` on response.body
// 3. We use a switch to explicitly call out both cases of decoding the Blob into ?Text
let str_body = String::from_utf8(response.body)
.expect("Transformed response is not UTF-8 encoded.");
//The API response will looks like this:
// ("[[1682978460,5.714,5.718,5.714,5.714,243.5678]]")
//Which can be formatted as this
// [
// [
// 1682978460, <-- start/timestamp
// 5.714, <-- low
// 5.718, <-- high
// 5.714, <-- open
// 5.714, <-- close
// 243.5678 <-- volume
// ],
// ]
//Return the body as a string and end the method
str_body
}
Err((r, m)) => {
let message =
format!("The http_request resulted into error. RejectionCode: {r:?}, Error: {m}");
//Return the error as a string and end the method
message
}
}
}
// Strips all data that is not needed from the original response.
#[query]
fn transform(raw: TransformArgs) -> HttpResponse {
let headers = vec![
HttpHeader {
name: "Content-Security-Policy".to_string(),
value: "default-src 'self'".to_string(),
},
HttpHeader {
name: "Referrer-Policy".to_string(),
value: "strict-origin".to_string(),
},
HttpHeader {
name: "Permissions-Policy".to_string(),
value: "geolocation=(self)".to_string(),
},
HttpHeader {
name: "Strict-Transport-Security".to_string(),
value: "max-age=63072000".to_string(),
},
HttpHeader {
name: "X-Frame-Options".to_string(),
value: "DENY".to_string(),
},
HttpHeader {
name: "X-Content-Type-Options".to_string(),
value: "nosniff".to_string(),
},
];
let mut res = HttpResponse {
status: raw.response.status.clone(),
body: raw.response.body.clone(),
headers,
..Default::default()
};
if res.status == 200 {
res.body = raw.response.body;
} else {
ic_cdk::api::print(format!("Received an error from coinbase: err = {:?}", raw));
}
res
}
get_icp_usd_exchange() -> String
returns aString
, but this is not necessary. In this tutorial, this is done for easier testing.- The
lib.rs
file uses http_request which is a convenient Rust CDK method that already sends cycles to the management canister under the hood. It knows how many cycles to send for a 13-node subnet in most cases. If your HTTPS outcall needs more cycles, you should use the http_request_with_cycles() method and explicitly call the cycles needed. - The Rust CDK method
http_request
used above wraps the management canister methodhttp_request
, but it is not strictly the same.
To use HTTPS outcalls you must update the canister's Candid file:
service : {
"get_icp_usd_exchange": () -> (text);
}
Update the Cargo.toml
file to use the correct dependencies:
[package]
name = "send_http_get_backend"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
candid = "0.8.2"
ic-cdk = "0.6.0"
ic-cdk-macros = "0.6.0"
serde = "1.0.152"
serde_json = "1.0.93"
serde_bytes = "0.11.9"
Headers in the response may not always be identical across all nodes that process the request for consensus, causing the result of the call to be "No consensus could be reached." This particular error message can be hard to debug, but one method to resolve this error is to edit the response using the transform function. The transform function is run before consensus, and can be used to remove some headers from the response.
You can see a deployed version of this canister and its get_icp_usd_exchange
method onchain here: https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=fm664-jyaaa-aaaap-qbomq-cai.
Additional resources
- Sample code of HTTP
GET
requests in Rust. - Sample code of HTTP
GET
requests in Motoko.