Skip to content

Commit 55043fe

Browse files
authored
Merge pull request #1315 from dfinity/docs/region-markers-https-outcalls
feat: modernize HTTPS outcalls examples with doc region markers
2 parents 0f3feb6 + f3f277d commit 55043fe

File tree

13 files changed

+192
-510
lines changed

13 files changed

+192
-510
lines changed

motoko/send_http_get/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ build:
99
.PHONY: test
1010
.SILENT: test
1111
test: build
12-
dfx canister call send_http_get_backend get_icp_usd_exchange \
13-
| grep '\[1682978460,5\.714,5\.718,5\.714,5\.714,243\.5678\]' && echo 'PASS'
12+
dfx canister call send_http_get_backend send_http_get_request \
13+
| grep 'hello-from-icp' && echo 'PASS'
1414

1515
.PHONY: clean
1616
.SILENT: clean
Lines changed: 31 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,49 @@
11
import Blob "mo:base/Blob";
2-
import Nat64 "mo:base/Nat64";
32
import Text "mo:base/Text";
43
import IC "ic:aaaaa-aa";
54

6-
//Actor
75
persistent actor {
86

9-
//This method sends a GET request to a URL with a free API we can test.
10-
//This method returns Coinbase data on the exchange rate between USD and ICP
11-
//for a certain day.
12-
//The API response looks like this:
13-
// [
14-
// [
15-
// 1682978460, <-- start timestamp
16-
// 5.714, <-- lowest price during time range
17-
// 5.718, <-- highest price during range
18-
// 5.714, <-- price at open
19-
// 5.714, <-- price at close
20-
// 243.5678 <-- volume of ICP traded
21-
// ],
22-
// ]
23-
24-
//function to transform the response
7+
// #region transform
8+
// Strip HTTP response headers (date, cookies, tracking IDs) that vary across replicas.
9+
// In replicated mode, all replicas must see an identical response for consensus to
10+
// succeed — the transform ensures this by discarding non-deterministic fields.
2511
public query func transform({
2612
context : Blob;
2713
response : IC.http_request_result;
2814
}) : async IC.http_request_result {
29-
{
30-
response with headers = []; // not interested in the headers
31-
};
15+
{ response with headers = [] };
3216
};
33-
34-
public func get_icp_usd_exchange() : async Text {
35-
36-
//1. SETUP ARGUMENTS FOR HTTP GET request
37-
let ONE_MINUTE : Nat64 = 60;
38-
let start_timestamp : Nat64 = 1682978460; //May 1, 2023 22:01:00 GMT
39-
let host : Text = "api.exchange.coinbase.com";
40-
let url = "https://" # host # "/products/ICP-USD/candles?start=" # Nat64.toText(start_timestamp) # "&end=" # Nat64.toText(start_timestamp) # "&granularity=" # Nat64.toText(ONE_MINUTE);
41-
42-
// 1.2 prepare headers for the system http_request call
43-
let request_headers = [
44-
{ name = "User-Agent"; value = "price-feed" },
45-
];
46-
47-
// 1.3 The HTTP request
48-
let http_request : IC.http_request_args = {
49-
url = url;
50-
max_response_bytes = null; //optional for request
51-
headers = request_headers;
52-
body = null; //optional for request
17+
// #endregion transform
18+
19+
// #region get_request
20+
public func send_http_get_request() : async Text {
21+
let request : IC.http_request_args = {
22+
url = "https://postman-echo.com/get?greeting=hello-from-icp";
23+
// Always set max_response_bytes to a tight bound. The cycle cost scales
24+
// with this value, not the actual response size. If omitted, the system
25+
// assumes 2MB. Unused cycles are refunded, but you still pay for the
26+
// declared maximum.
27+
max_response_bytes = ?(3_000 : Nat64);
28+
headers = [{ name = "User-Agent"; value = "ic-canister" }];
29+
body = null;
5330
method = #get;
54-
transform = ?{
55-
function = transform;
56-
context = Blob.fromArray([]);
57-
};
58-
// Toggle this flag to switch between replicated and non-replicated http outcalls.
59-
is_replicated = ?false;
31+
transform = ?{ function = transform; context = Blob.fromArray([]) };
32+
// Replicated mode: all subnet nodes make the request independently,
33+
// providing strong integrity guarantees via consensus.
34+
is_replicated = ?true;
6035
};
6136

62-
//2. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE, BUT MAKE SURE TO ADD CYCLES.
63-
let http_response : IC.http_request_result = await (with cycles = 230_949_972_000) IC.http_request(http_request);
37+
// Cycles must be explicitly attached to management canister calls.
38+
// The amount is based on request size and max_response_bytes.
39+
let response = await (with cycles = 230_949_972_000) IC.http_request(request);
6440

65-
//3. DECODE THE RESPONSE
66-
67-
//As per the type declarations, the BODY in the HTTP response
68-
//comes back as Blob. Type signature:
69-
70-
//public type http_request_result = {
71-
// status : Nat;
72-
// headers : [HttpHeader];
73-
// body : Blob;
74-
// };
75-
76-
//We need to decode that Blob that is the body into readable text.
77-
//To do this, we:
78-
// 1. Use Text.decodeUtf8() method to convert the Blob to a ?Text optional
79-
// 2. We use a switch to explicitly call out both cases of decoding the Blob into ?Text
80-
let decoded_text : Text = switch (Text.decodeUtf8(http_response.body)) {
81-
case (null) { "No value returned" };
82-
case (?y) { y };
41+
// postman-echo.com echoes back the request metadata as JSON, letting you
42+
// verify the query params and headers were sent correctly.
43+
switch (Text.decodeUtf8(response.body)) {
44+
case (?text) text;
45+
case null "Response is not valid UTF-8";
8346
};
84-
85-
//4. RETURN RESPONSE OF THE BODY
86-
//The API response will looks like this:
87-
//
88-
// ("[[1682978460,5.714,5.718,5.714,5.714,243.5678]]")
89-
//
90-
//The API response looks like this:
91-
// [
92-
// [
93-
// 1682978460, <-- start timestamp
94-
// 5.714, <-- lowest price during time range
95-
// 5.718, <-- highest price during range
96-
// 5.714, <-- price at open
97-
// 5.714, <-- price at close
98-
// 243.5678 <-- volume of ICP traded
99-
// ],
100-
// ]
101-
decoded_text;
10247
};
103-
48+
// #endregion get_request
10449
};

motoko/send_http_post/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ build: node_modules
1515
.SILENT: test
1616
test: deploy
1717
dfx canister call send_http_post_backend send_http_post_request \
18-
| grep 'Hello' && echo 'PASS'
18+
| grep 'POST request from an ICP canister' && echo 'PASS'
1919

2020
.PHONY: clean
2121
.SILENT: clean

motoko/send_http_post/src/send_http_post_backend/main.mo

Lines changed: 34 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -4,92 +4,52 @@ import IC "ic:aaaaa-aa";
44

55
persistent actor {
66

7-
//function to transform the response
7+
// #region transform
8+
// Strip HTTP response headers (date, cookies, tracking IDs) that vary across requests.
9+
// Even in non-replicated mode (used here), the transform is required — the system
10+
// always invokes it. In replicated mode, stripping non-deterministic fields is
11+
// essential for consensus to succeed.
812
public query func transform({
913
context : Blob;
1014
response : IC.http_request_result;
1115
}) : async IC.http_request_result {
12-
{
13-
response with headers = []; // not interested in the headers
14-
};
16+
{ response with headers = [] };
1517
};
18+
// #endregion transform
1619

17-
//PULIC METHOD
18-
//This method sends a POST request to a URL with a free API we can test.
20+
// #region post_request
1921
public func send_http_post_request() : async Text {
20-
21-
//1. SETUP ARGUMENTS FOR HTTP GET request
22-
23-
// 1.1 Setup the URL and its query parameters
24-
//This URL is used because it allows us to inspect the HTTP request sent from the canister
25-
let host : Text = "putsreq.com";
26-
let url = "https://putsreq.com/XaTKyDHZ0O04gkgQwBKz"; //HTTP that accepts IPV6
27-
28-
// 1.2 prepare headers for the system http_request call
29-
30-
//idempotency keys should be unique so we create a function that generates them.
31-
let idempotency_key : Text = generateUUID();
32-
let request_headers = [
33-
{ name = "User-Agent"; value = "http_post_sample" },
34-
{ name = "Content-Type"; value = "application/json" },
35-
{ name = "Idempotency-Key"; value = idempotency_key },
36-
];
37-
38-
// The request body is a Blob, so we do the following:
39-
// 1. Write a JSON string
40-
// 2. Convert Text into a Blob
41-
let request_body_json : Text = "{ \"name\" : \"Grogu\", \"force_sensitive\" : \"true\" }";
42-
let request_body = Text.encodeUtf8(request_body_json);
43-
44-
// 1.3 The HTTP request
45-
let http_request : IC.http_request_args = {
46-
url = url;
47-
max_response_bytes = null; //optional for request
48-
headers = request_headers;
49-
//note: type of `body` is ?Blob so we pass it here as "?request_body" instead of "request_body"
50-
body = ?request_body;
22+
let body = Text.encodeUtf8("This is a POST request from an ICP canister.");
23+
24+
let request : IC.http_request_args = {
25+
url = "https://postman-echo.com/post";
26+
// Always set max_response_bytes to a tight bound. The cycle cost scales
27+
// with this value, not the actual response size. If omitted, the system
28+
// assumes 2MB. Unused cycles are refunded, but you still pay for the
29+
// declared maximum.
30+
max_response_bytes = ?(3_000 : Nat64);
31+
headers = [
32+
{ name = "Content-Type"; value = "text/plain" },
33+
];
34+
body = ?body;
5135
method = #post;
52-
transform = ?{
53-
function = transform;
54-
context = Blob.fromArray([]);
55-
};
56-
// Toggle this flag to switch between replicated and non-replicated http outcalls.
36+
transform = ?{ function = transform; context = Blob.fromArray([]) };
37+
// Non-replicated: only one replica sends the request. For replicated
38+
// mode (true), add an Idempotency-Key header so the server can
39+
// deduplicate the requests sent by each replica independently.
5740
is_replicated = ?false;
5841
};
5942

60-
//2. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE, BUT MAKE SURE TO ADD CYCLES.
61-
let http_response : IC.http_request_result = await (with cycles = 230_949_972_000) IC.http_request(http_request);
43+
// Cycles must be explicitly attached to management canister calls.
44+
// The amount is based on request size and max_response_bytes.
45+
let response = await (with cycles = 230_949_972_000) IC.http_request(request);
6246

63-
//3. DECODE THE RESPONSE
64-
65-
//As per the type declarations, the BODY in the HTTP response
66-
//comes back as Blob. Type signature:
67-
68-
//public type http_request_result = {
69-
// status : Nat;
70-
// headers : [HttpHeader];
71-
// body : Blob;
72-
// };
73-
74-
//We need to decode that Blob that is the body into readable text.
75-
//To do this, we:
76-
// 1. Use Text.decodeUtf8() method to convert the Blob to a ?Text optional
77-
// 2. We use a switch to explicitly call out both cases of decoding the Blob into ?Text
78-
let decoded_text : Text = switch (Text.decodeUtf8(http_response.body)) {
79-
case (null) { "No value returned" };
80-
case (?y) { y };
47+
// postman-echo.com echoes back the request data as JSON, letting you
48+
// verify the POST body and headers were sent correctly.
49+
switch (Text.decodeUtf8(response.body)) {
50+
case (?text) text;
51+
case null "Response is not valid UTF-8";
8152
};
82-
83-
//4. RETURN RESPONSE OF THE BODY
84-
let result : Text = decoded_text # ". See more info of the request sent at: " # url # "/inspect";
85-
result;
86-
};
87-
88-
//PRIVATE HELPER FUNCTION
89-
//Helper method that generates a Universally Unique Identifier
90-
//this method is used for the Idempotency Key used in the request headers of the POST request.
91-
//For the purposes of this exercise, it returns a constant, but in practice it should return unique identifiers
92-
func generateUUID() : Text {
93-
"UUID-123456789";
9453
};
54+
// #endregion post_request
9555
};

0 commit comments

Comments
 (0)