Skip to main content

How to use HTTPS outcalls: POST

Advanced
Tutorial

Overview

A minimal example of how to make a POST HTTP request. The purpose of this dapp is only to show how to make HTTP requests from a canister. It sends a POST request with some JSON to a free API where you can verify the headers and body were sent correctly.

The HTTPS outcalls feature only works for sending HTTP POST requests to servers or API endpoints that support IPV6.

In order to verify that your canister sent the HTTP request you expected, this canister is sending HTTP requests to a public API service where the HTTP request can be inspected. As you can see the image below, the POST request headers and body can be inspected to make sure it is what the canister sent.

Public API to inspect POST request

Important notes on POST requests

Because HTTP outcalls go through consensus, a developer should expect any HTTP POST request from a canister to be sent many times to its destination. Even if you ignore the Web3 component, multiple identical POST requests are not a new problem in HTTPS where it is common for clients to retry requests for a variety of reasons (e.g. destination server being unavailable).

The recommended way for HTTP POST requests is to add the idempotency keys in the header so the destination server knows which POST requests from the client are the same.

Developers should be careful that the destination server understands and uses idempotency keys. A canister can be coded to send idempotency keys, but it is ultimately up to the recipient server to know what to do with them. Here is an example of an API service that uses idempotency keys.

POST example

import Debug "mo:base/Debug";
import Blob "mo:base/Blob";
import Cycles "mo:base/ExperimentalCycles";
import Array "mo:base/Array";
import Nat8 "mo:base/Nat8";
import Text "mo:base/Text";

//import the custom types we have in Types.mo
import Types "Types";

actor {

//function to transform the response
public query func transform(raw : Types.TransformArgs) : async Types.CanisterHttpResponsePayload {
let transformed : Types.CanisterHttpResponsePayload = {
status = raw.response.status;
body = raw.response.body;
headers = [
{
name = "Content-Security-Policy";
value = "default-src 'self'";
},
{
name = "Referrer-Policy";
value = "strict-origin"
},
{
name = "Permissions-Policy";
value = "geolocation=(self)" },
{
name = "Strict-Transport-Security";
value = "max-age=63072000";
},
{
name = "X-Frame-Options";
value = "DENY"
},
{
name = "X-Content-Type-Options";
value = "nosniff"
},
];
};
transformed;
};

//PULIC METHOD
//This method sends a POST request to a URL with a free API we can test.
public func send_http_post_request() : async Text {

//1. DECLARE IC MANAGEMENT CANISTER
//We need this so we can use it to make the HTTP request
let ic : Types.IC = actor ("aaaaa-aa");

//2. SETUP ARGUMENTS FOR HTTP GET request

// 2.1 Setup the URL and its query parameters
//This URL is used because it allows us to inspect the HTTP request sent from the canister
let host : Text = "putsreq.com";
let url = "https://putsreq.com/aL1QS5IbaQd4NTqN3a81"; //HTTP that accepts IPV6

// 2.2 prepare headers for the system http_request call

//idempotency keys should be unique so we create a function that generates them.
let idempotency_key: Text = generateUUID();
let request_headers = [
{ name = "Host"; value = host # ":443" },
{ name = "User-Agent"; value = "http_post_sample" },
{ name= "Content-Type"; value = "application/json" },
{ name= "Idempotency-Key"; value = idempotency_key }
];

// The request body is an array of [Nat8] (see Types.mo) so we do the following:
// 1. Write a JSON string
// 2. Convert ?Text optional into a Blob, which is an intermediate reprepresentation before we cast it as an array of [Nat8]
// 3. Convert the Blob into an array [Nat8]
let request_body_json: Text = "{ \"name\" : \"Grogu\", \"force_sensitive\" : \"true\" }";
let request_body_as_Blob: Blob = Text.encodeUtf8(request_body_json);
let request_body_as_nat8: [Nat8] = Blob.toArray(request_body_as_Blob); // e.g [34, 34,12, 0]


// 2.2.1 Transform context
let transform_context : Types.TransformContext = {
function = transform;
context = Blob.fromArray([]);
};

// 2.3 The HTTP request
let http_request : Types.HttpRequestArgs = {
url = url;
max_response_bytes = null; //optional for request
headers = request_headers;
//note: type of `body` is ?[Nat8] so we pass it here as "?request_body_as_nat8" instead of "request_body_as_nat8"
body = ?request_body_as_nat8;
method = #post;
transform = ?transform_context;
};

//3. 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
Cycles.add(230_850_258_000);

//4. MAKE HTTPS REQUEST AND WAIT FOR RESPONSE
//Since the cycles were added above, we can just call the IC management canister with HTTPS outcalls below
let http_response : Types.HttpResponsePayload = await ic.http_request(http_request);

//5. DECODE THE RESPONSE

//As per the type declarations in `Types.mo`, the BODY in the HTTP response
//comes back as [Nat8s] (e.g. [2, 5, 12, 11, 23]). Type signature:

//public type HttpResponsePayload = {
// status : Nat;
// headers : [HttpHeader];
// body : [Nat8];
// };

//We need to decode that [Na8] array that is the body into readable text.
//To do this, we:
// 1. Convert the [Nat8] into a Blob
// 2. Use Blob.decodeUtf8() method to convert the Blob to a ?Text optional
// 3. We use Motoko syntax "Let... else" to unwrap what is returned from Text.decodeUtf8()
let response_body: Blob = Blob.fromArray(http_response.body);
let decoded_text: Text = switch (Text.decodeUtf8(response_body)) {
case (null) { "No value returned" };
case (?y) { y };
};

//6. RETURN RESPONSE OF THE BODY
let result: Text = decoded_text # ". See more info of the request sent at: " # url # "/inspect";
result
};

//PRIVATE HELPER FUNCTION
//Helper method that generates a Universally Unique Identifier
//this method is used for the Idempotency Key used in the request headers of the POST request.
//For the purposes of this exercise, it returns a constant, but in practice it should return unique identifiers
func generateUUID() : Text {
"UUID-123456789";
}
};

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's send_http_post_request method onchain here: https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=fm664-jyaaa-aaaap-qbomq-cai.

Additional resources