https://fxa77-fiaaa-aaaae-aaana-cai.raw.ic0.app/
Hello, World Wide Web!
It’s great that we can run dfx to make an Internet Computer call to the hi
method of our Hello, Candid! canister to see a message.
However, people prefer to use browsers to do this sort of thing.
It turns out we already can: canisters understand HTTPS, and indeed, the dfx
tool speaks HTTPS to call canisters. We could write some JavaScript to do the
same, and put it in a webpage. The
agent-js repo contains code that does exactly that, and we’ll build our own
agent eventually.
However, there’s a far simpler solution, provided our function is cheap and
read-only. If we export a function with the magic name
canister_query http_request, then visting an URL like:
calls our function with a Candid message of type:
type HeaderField = record { text; text; };
type HttpRequest = record {
method: text;
url: text;
headers: vec HeaderField;
body: blob;
};
and expects a reply in Candid format of type:
type HttpResponse = record {
status_code: nat16;
headers: vec HeaderField;
body: blob;
};
which is decoded and returned to the browser.
The HttpResponse record also has an optional streaming_strategy field; the
full definition appears in
the
file internet_identity.did.
Therefore, to make a minimal canister that works in a browser, we change our Hello, Candid! example so that:
-
The export is named
canister_query http_request. -
Instead returning a
blobortext, we return aHttpResponserecord withstatus_code200 and thebodyset to our desired message.
The result is helloweb.c:
#define IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n)));
#define EXPORT(n) asm(n) __attribute__((visibility("default")))
void reply_append(void*, unsigned) IMPORT("ic0", "msg_reply_data_append");
void reply (void) IMPORT("ic0", "msg_reply");
void go() EXPORT("canister_query http_request");
void go() {
char msg[] = "DIDL\x03"
"\x6c\x03"
"\xa2\xf5\xed\x88\x04\x01"
"\xc6\xa4\xa1\x98\x06\x02"
"\x9a\xa1\xb2\xf9\x0c\x7a"
"\x6d\x7b"
"\x6d\x6f"
"\x01\x00\x16Hello, World Wide Web!\x00\xc8\x00";
reply_append(msg, sizeof(msg) - 1);
reply();
}
Candid Field Hash
This Candid message is more complex than our previous example, and shows off a few unusual features of the format. Let’s look at the beginning:
44 49 44 4c: magic header
03: type table of size 3
6c 03: [type #0] record of size 3
a2 f5 ed 88 04: field with hash 1092319906
01: type #1
c6 a4 a1 98 06: field with hash 1661489734
02: type #2
9a a1 b2 f9 0c: field with hash 3475804314
7a: nat16
6d: [type #1] vec
7b: nat8
6d: [type #2] vec
6f: empty
Each of the 3 record field names is replaced by its unsigned 32-bit hash, which the following tool can compute:
Candid field hash:
Incidentally, the source of this widget is essentially the Haskell function:
fieldHash :: String -> Word32
fieldHash s = sum $ zipWith (*) (reverse ns) $ iterate (223*) 1
where ns = fromIntegral . ord <$> s
So really there is no status_code field; there is only 3475804314. On the
one hand, this scheme is efficient. A field name never costs more than 32 bits.
On the other hand, from a Candid message alone, we only have the hashes, and
not the original human-friendly names.
The field hashes must appear in increasing order, and are encoded with
LEB128. For example, 3475804314
is encoded as 9a a1 b2 f9 0c.
Another peculiarity is that field 1661489734, which is the hash of headers,
has type vec empty, even though the headers field of a HttpResponse is
supposed to have type vec HeaderField.
This is legal due to Candid’s subtyping feature, and often lets us declare only the things we care about.
The argument section of the Candid message is straightforward:
01: arg count of 1
00: [arg #0] type #0
arg #0:
record:
16: vec of length 22
"Hello, World Wide Web!\n": contents of the vector
00: vec of length 0
c8 00: nat16
In other words, we have a body containing our message, no headers, and a
status_code of 0x00c8 = 200.
Testing
We follow the usual steps to build and deploy this canister,
but this time, to test it, instead of running dfx, we point a browser to
something like:
http://localhost:8000/?canisterId=rrkah-fqaaa-aaaaa-aaaaq-cai
The canister ID depends on how many canisters have already been deployed, so
may differ from the one shown. Just copy and paste the canister ID shown after
running dfx deploy. Alternatively, look it up in
.dfx/local/canister_ids.json.
After deployed to production, we would instead navigate to something like:
https://rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app/
This works via boundary nodes, which translate incoming HTTPS requests to
canister calls to the query method http_request, and then translate canister
replies to HTTPS responses.
We can also test with dfx:
$ dfx canister call helloweb http_request --output raw
4449444c036c03a2f5ed880401c6a4a19806029aa1b2f90c7a6d7b6d6f01001648656c6c6f2c20576f726c642057696465205765622100c800
$ dfx canister call helloweb http_request
(
record {
1_092_319_906 = blob "Hello, World Wide Web!";
1_661_489_734 = vec {};
3_475_804_314 = 200 : nat16;
},
)
See Also
Thanks to the Internet Computer, deploying a simple robust webserver is mostly a matter of writing a C program around a print statement. (Contrast this with typical cloud computing services: back when I used one, I had to install and maintain an operating system before deploying any servers, and I never bothered setting up redundant servers, so if my single machine went offline, so would my website.)
We can automate this process with a script that produces a C program like the one above, then compiles it to a canister.
For less trivial servers, we’ll want to do examine the HttpRequest. What do
these look like? One way to answer this is to write a program that replies with
the HttpRequest message it received, say, hex-encoded so a browser can show
it. This is an exercise left to the reader; for a possible answer, see
this echo server.
The next step is serving more than one endpoint. The canister running this
website extracts the URL path from the HttpRequest message, which determines
the reply. We build it via a script that generates a website
canister from a list of files.