Handling GET/POST requests
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.
- Motoko
- Rust
- TypeScript
- Python
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.
use http::{Request, Response, StatusCode};
#[query]
fn respond_to(req: Request<()>) -> http::Result<Response<()>> {
if req.uri() != "/hello" {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.body(())
}
let response_header = req.headers().contains_key("Hello!");
let body = req.body();
}
Check out the Rust documentation for more info on query calls.
import {
call,
candidEncode,
id,
IDL,
Principal,
query,
reply,
update
} from 'azle';
import {
HttpRequestArgs,
HttpResponse,
HttpTransformArgs
} from 'azle/canisters/management';
export default class {
@update([], IDL.Text)
async xkcd(): Promise<string> {
const httpResponse = await call('aaaaa-aa', 'http_request', {
paramIdlTypes: [HttpRequestArgs],
returnIdlType: HttpResponse,
args: [
{
url: `https://xkcd.com/642/info.0.json`,
max_response_bytes: [2_000n],
method: {
get: null
},
headers: [],
body: [],
transform: [
{
function: [id(), 'xkcdTransform'],
context: Uint8Array.from([])
}
]
}
],
payment: 50_000_000n
});
return Buffer.from(httpResponse.body).toString();
}
@update([], HttpResponse, { manual: true })
async xkcdRaw(): Promise<void> {
const httpResponse = await call(
Principal.fromText('aaaaa-aa'),
'http_request',
{
raw: candidEncode(`
(
record {
url = "https://xkcd.com/642/info.0.json";
max_response_bytes = 2_000 : nat64;
method = variant { get };
headers = vec {};
body = null;
transform = record { function = func "${id().toString()}".xkcdTransform; context = vec {} };
}
)
`),
payment: 50_000_000n
}
);
reply({ raw: httpResponse });
}
@query([HttpTransformArgs], HttpResponse)
xkcdTransform(args: HttpTransformArgs): HttpResponse {
return {
...args.response,
headers: []
};
}
}
Check out the Azle documentation for more info on HTTP requests.
from kybra import (
Alias,
blob,
Func,
ic,
nat,
nat16,
Opt,
Query,
query,
Record,
StableBTreeMap,
Tuple,
Variant,
Vec,
)
class Token(Record):
arbitrary_data: str
class StreamingCallbackHttpResponse(Record):
body: blob
token: Opt[Token]
Callback = Func(Query[[Token], StreamingCallbackHttpResponse])
class CallbackStrategy(Record):
callback: Callback
token: Token
class StreamingStrategy(Variant, total=False):
Callback: CallbackStrategy
HeaderField = Alias[Tuple[str, str]]
class HttpResponse(Record):
status_code: nat16
headers: Vec[HeaderField]
body: blob
streaming_strategy: Opt[StreamingStrategy]
upgrade: Opt[bool]
class HttpRequest(Record):
method: str
url: str
headers: Vec[HeaderField]
body: blob
certificate_version: Opt[nat16]
stable_storage = StableBTreeMap[str, nat](
memory_id=0, max_key_size=15, max_value_size=1_000
)
stable_storage.insert("counter", 0)
def isGzip(x: HeaderField) -> bool:
return x[0].lower() == "accept-encoding" and "gzip" in x[1].lower()
@query
def http_request(req: HttpRequest) -> HttpResponse:
ic.print("Hello from http_request")
if req["method"] == "GET":
if next(filter(isGzip, req["headers"]), None) is None:
if req["url"] == "/stream":
return {
"status_code": 200,
"headers": [("content-type", "text/plain")],
"body": "Counter".encode("utf-8"),
"streaming_strategy": {
"Callback": {
"callback": (ic.id(), "http_streaming"),
"token": {"arbitrary_data": "start"},
}
},
"upgrade": False,
}
return {
"status_code": 200,
"headers": [("content-type", "text/plain")],
"body": f"Counter is {stable_storage.get('counter')}\n{req['url']}".encode(
"utf-8"
),
"streaming_strategy": None,
"upgrade": None,
}
return {
"status_code": 200,
"headers": [("content-type", "text/plain"), ("content-encoding", "gzip")],
"body": bytes(
[
31,
139,
8,
0,
152,
2,
27,
98,
0,
3,
43,
44,
77,
45,
170,
228,
2,
0,
214,
128,
43,
5,
6,
0,
0,
0,
]
),
"streaming_strategy": None,
"upgrade": None,
}
if req["method"] == "POST":
return {
"status_code": 204,
"headers": [],
"body": "".encode("utf-8"),
"streaming_strategy": None,
"upgrade": True,
}
return {
"status_code": 400,
"headers": [],
"body": "Invalid request".encode("utf-8"),
"streaming_strategy": None,
"upgrade": None,
}
Kybra canisters must be deployed from a Python virtual environment. Learn more in the Kybra docs.
Check out the Kybra documentation for more info on HTTP requests.
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.
- Motoko
- Rust
- TypeScript
- Python
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.
use http::{Request, Response, StatusCode};
#[update]
fn respond_to(req: Request<()>) -> http::Result<Response<()>> {
let mut builder = Response::builder()
.header("Hello!")
.status(StatusCode::OK);
if req.headers().contains_key("Another-Header") {
builder = builder.header("Another-Header", "Ack");
}
builder.body(())
}
Check out the Rust documentation for more info on update calls.
import {
call,
candidEncode,
id,
IDL,
Principal,
query,
reply,
update
} from 'azle';
import {
HttpRequestArgs,
HttpResponse,
HttpTransformArgs
} from 'azle/canisters/management';
export default class {
@update([], IDL.Text)
async xkcd(): Promise<string> {
const httpResponse = await call('aaaaa-aa', 'http_request', {
paramIdlTypes: [HttpRequestArgs],
returnIdlType: HttpResponse,
args: [
{
url: `https://xkcd.com/642/info.0.json`,
max_response_bytes: [2_000n],
method: {
get: null
},
headers: [],
body: [],
transform: [
{
function: [id(), 'xkcdTransform'] as [
Principal,
string
],
context: Uint8Array.from([])
}
]
}
],
payment: 50_000_000n
});
return Buffer.from(httpResponse.body).toString();
}
@update([], HttpResponse, { manual: true })
async xkcdRaw(): Promise<void> {
const httpResponse = await call(
Principal.fromText('aaaaa-aa'),
'http_request',
{
raw: candidEncode(`
(
record {
url = "https://xkcd.com/642/info.0.json";
max_response_bytes = 2_000 : nat64;
method = variant { post };
headers = vec {};
body = null;
transform = record { function = func "${id().toString()}".xkcdTransform; context = vec {} };
}
)
`),
payment: 50_000_000n
}
);
reply({ raw: httpResponse });
}
@query([HttpTransformArgs], HttpResponse)
xkcdTransform(args: HttpTransformArgs): HttpResponse {
return {
...args.response,
headers: []
};
}
}
Check out the Azle documentation for more info on HTTP requests.
from kybra import (
Alias,
blob,
Func,
ic,
nat,
nat16,
Opt,
Query,
query,
Record,
StableBTreeMap,
Tuple,
update,
Variant,
Vec,
)
class Token(Record):
arbitrary_data: str
class StreamingCallbackHttpResponse(Record):
body: blob
token: Opt[Token]
Callback = Func(Query[[Token], StreamingCallbackHttpResponse])
class CallbackStrategy(Record):
callback: Callback
token: Token
class StreamingStrategy(Variant, total=False):
Callback: CallbackStrategy
HeaderField = Alias[Tuple[str, str]]
class HttpResponse(Record):
status_code: nat16
headers: Vec[HeaderField]
body: blob
streaming_strategy: Opt[StreamingStrategy]
upgrade: Opt[bool]
class HttpRequest(Record):
method: str
url: str
headers: Vec[HeaderField]
body: blob
stable_storage = StableBTreeMap[str, nat](
memory_id=0, max_key_size=15, max_value_size=1_000
)
stable_storage.insert("counter", 0)
def isGzip(x: HeaderField) -> bool:
return x[0].lower() == "accept-encoding" and "gzip" in x[1].lower()
@update
def http_request_update(req: HttpRequest) -> HttpResponse:
ic.print("Hello from update")
global stable_storage
if req["method"] == "POST":
counter = stable_storage.get("counter") or 0
stable_storage.insert("counter", counter + 1)
if next(filter(isGzip, req["headers"]), None) is None:
return {
"status_code": 201,
"headers": [("content-type", "text/plain")],
"body": f"Counter updated to {stable_storage.get('counter')}".encode(
"utf-8"
),
"streaming_strategy": None,
"upgrade": None,
}
return {
"status_code": 201,
"headers": [("content-type", "text/plain"), ("content-encoding", "gzip")],
"body": bytes(
[
31,
139,
8,
0,
55,
2,
27,
98,
0,
3,
43,
45,
72,
73,
44,
73,
229,
2,
0,
168,
218,
145,
108,
7,
0,
0,
0,
]
),
"streaming_strategy": None,
"upgrade": None,
}
return {
"status_code": 400,
"headers": [],
"body": "Invalid request".encode("utf-8"),
"streaming_strategy": None,
"upgrade": None,
}
@query
def http_streaming(token: Token) -> StreamingCallbackHttpResponse:
ic.print("Hello from http_streaming")
if token["arbitrary_data"] == "start":
return {"body": " is ".encode("utf-8"), "token": {"arbitrary_data": "next"}}
if token["arbitrary_data"] == "next":
return {
"body": f"{stable_storage.get('counter')}".encode("utf-8"),
"token": {"arbitrary_data": "last"},
}
if token["arbitrary_data"] == "last":
return {"body": " streaming\n".encode("utf-8"), "token": None}
ic.trap("unreachable")
Kybra canisters must be deployed from a Python virtual environment. Learn more in the Kybra docs.
Check out the Kybra documentation for more info on HTTP requests.