Skip to main content

Handling GET/POST requests

Advanced
Tutorial

Overview

Canisters running on ICP can use HTTP requests in two ways: incoming and outgoing. Incoming HTTP requests refer to HTTP requests that are sent to a canister and can be used to retrieve data from a canister or send new data to the canister. Outgoing HTTP requests refer to HTTP requests that the canister sends to other canisters or external services to retrieve data or send new data.

Outgoing HTTP requests

For outgoing HTTP requests, the HTTPS outcalls feature should be used.

Incoming HTTP requests

To handle incoming HTTP requests, canisters must define methods for http_requests and http_requests_update for GET and POST requests respectively.

All HTTP requests are handled by the ICP HTTP Gateway, therefore you cannot make direct POST calls to a canister's http_request_update method with HTTP clients such as curl. Instead, you can make a POST call to a canister's HTTP endpoint, then configure the canister's http_request method to upgrade the call to http_request_update if necessary. Below is an example POST call to a canister's endpoint:

curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' https://<canister-id>.raw.ic0.app/<endpoint>

GET requests

HTTP GET requests are used to retrieve and return existing data from an endpoint. To handle a canister's incoming GET requests, the http_request method can be exposed. Users and other services can call this method using a query call. To return HTTP response data, the following examples display how to configure the http_request to return an HTTP GET request.

In Motoko, a case configuration can be used to return different GET responses based on the endpoint:

import FHM "mo:StableHashMap/FunctionalStableHashMap";
import SHA256 "mo:motoko-sha/SHA256";
import CertTree "mo:ic-certification/CertTree";
import CanisterSigs "mo:ic-certification/CanisterSigs";
import CertifiedData "mo:base/CertifiedData";
import HTTP "./Http";
import Iter "mo:base/Iter";
import Blob "mo:base/Blob";
import Option "mo:base/Option";
import Time "mo:base/Time";
import Text "mo:base/Text";
import Debug "mo:base/Debug";
import Prelude "mo:base/Prelude";
import Principal "mo:base/Principal";
import Buffer "mo:base/Buffer";
import Nat8 "mo:base/Nat8";
import CertifiedCache "lib";
import Int "mo:base/Int";

actor Self {
type HttpRequest = HTTP.HttpRequest;
type HttpResponse = HTTP.HttpResponse;

var two_days_in_nanos = 2 * 24 * 60 * 60 * 1000 * 1000 * 1000;

stable var entries : [(Text, (Blob, Nat))] = [];
var cache = CertifiedCache.fromEntries<Text, Blob>(
entries,
Text.equal,
Text.hash,
Text.encodeUtf8,
func(b : Blob) : Blob { b },
two_days_in_nanos + Int.abs(Time.now()),
);

public query func http_request(req : HttpRequest) : async HttpResponse {
switch (req.method, not Option.isNull(Array.find(req.headers, isGzip)), req.url) {
case ("GET", false, "/stream") {{
status_code = 200;
headers = [ ("content-type", "text/plain") ];
body = Text.encodeUtf8("Counter");
streaming_strategy = ?#Callback({
callback = http_streaming;
token = {
arbitrary_data = "start";
}
});
upgrade = ?false;
}};
case ("GET", false, _) {{
status_code = 200;
headers = [ ("content-type", "text/plain") ];
body = Text.encodeUtf8("Counter is " # Nat.toText(counter) # "\n" # req.url # "\n");
streaming_strategy = null;
upgrade = null;
}};
case ("GET", true, _) {{
status_code = 200;
headers = [ ("content-type", "text/plain"), ("content-encoding", "gzip") ];
body = "\1f\8b\08\00\98\02\1b\62\00\03\2b\2c\4d\2d\aa\e4\02\00\d6\80\2b\05\06\00\00\00";
streaming_strategy = null;
upgrade = null;
}};
};
}
}

Check out the certified cache example project to see this code in use.

POST requests

HTTP POST requests are used to send data to an endpoint with the intention of retaining that data. To handle incoming POST requests, the http_request_update method can be used. This method uses an update call, which can be used to change a canister's state. The following examples display how to configure http_request_update method within your canister.

In Motoko, a case configuration can be used to return different POST responses based on the endpoint:

import FHM "mo:StableHashMap/FunctionalStableHashMap";
import SHA256 "mo:motoko-sha/SHA256";
import CertTree "mo:ic-certification/CertTree";
import CanisterSigs "mo:ic-certification/CanisterSigs";
import CertifiedData "mo:base/CertifiedData";
import HTTP "./Http";
import Iter "mo:base/Iter";
import Blob "mo:base/Blob";
import Option "mo:base/Option";
import Time "mo:base/Time";
import Text "mo:base/Text";
import Debug "mo:base/Debug";
import Prelude "mo:base/Prelude";
import Principal "mo:base/Principal";
import Buffer "mo:base/Buffer";
import Nat8 "mo:base/Nat8";
import CertifiedCache "lib";
import Int "mo:base/Int";

actor Self {
type HttpRequest = HTTP.HttpRequest;
type HttpResponse = HTTP.HttpResponse;

var two_days_in_nanos = 2 * 24 * 60 * 60 * 1000 * 1000 * 1000;

stable var entries : [(Text, (Blob, Nat))] = [];
var cache = CertifiedCache.fromEntries<Text, Blob>(
entries,
Text.equal,
Text.hash,
Text.encodeUtf8,
func(b : Blob) : Blob { b },
two_days_in_nanos + Int.abs(Time.now()),
);

public func http_request_update(req : HttpRequest) : async HttpResponse {
switch (req.method, not Option.isNull(Array.find(req.headers, isGzip))) {
case ("POST", false) {
counter += 1;
{
status_code = 201;
headers = [ ("content-type", "text/plain") ];
body = Text.encodeUtf8("Counter updated to " # Nat.toText(counter) # "\n");
streaming_strategy = null;
upgrade = null;
}
};
case ("POST", true) {
counter += 1;
{
status_code = 201;
headers = [ ("content-type", "text/plain"), ("content-encoding", "gzip") ];
body = "\1f\8b\08\00\37\02\1b\62\00\03\2b\2d\48\49\2c\49\e5\02\00\a8\da\91\6c\07\00\00\00";

streaming_strategy = null;
upgrade = null;
}
};
};
};
}

Check out the certified cache example project to see this code in use.

Resources