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:

https://fxa77-fiaaa-aaaae-aaana-cai.raw.ic0.app/

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 blob or text, we return a HttpResponse record with status_code 200 and the body set 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.)

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.


💡