Skip to main content

Upgrading HTTP calls to update calls

This guide walks through an example project that demonstrates how to use the "Upgrade to Update call" feature of the HTTP gateway.

Since browsers are unable to directly interact with the ICP network, the HTTP gateway acts as a bridge between the two. The HTTP gateway forwards requests from clients to canisters and forwards responses from canisters back to clients. Before returning responses from the canister back to clients, the HTTP gateway verifies the certification of the response to ensure that they have not been tampered with.

Upgrading query calls to upgrade calls allows for the certification of any kind of dynamic response by leveraging ICP's consensus protocol without having to statically certify the response ahead of time. This is the simplest way to add secure HTTP support to a canister.

A similarly simple yet more performant, but insecure approach is to skip certification entirely. This is not recommended unless you are absolutely sure that certification really does not make sense for your canister. Check the "Skipping certification for HTTP responses" guide for more details on how to do that.

How it works

When the HTTP gateway receives a request from a client, it will forward the request to the target canister's http_request method as a query call. To upgrade this query call to an update call, the canister returns a response that sets the optional upgrade field to opt true. Omitting this field, or setting it to opt false will result in the HTTP Gateway treating the query call response as-is, without upgrading.

Upon receiving a response from the canister with the upgrade field set to opt true, the HTTP gateway will repeat the original request as an update call to the http_request_update method of the canister. The canister can then respond to the update call with any dynamic response and leverage the ICP consensus protocol for security. The certification resulting from putting this response through consensus will be verified by the HTTP Gateway to ensure it has not been tampered with.

Rust

This example project features both Rust and Motoko code. If you would rather follow the Motoko version, you can skip this section and go straight to the section covering Motoko.

The Rust code is split into two functions: http_request and http_request_update. The http_request function is the entry point for the query call from the HTTP Gateway. It returns an HttpResponse with the upgrade field set to Some(true) (via the build_update method on the HttpResponse::builder struct). The http_request_update function is the entry point for the update call from the HTTP Gateway. It returns an HttpUpdateResponse with a custom status code and body.

use ic_cdk::*;
use ic_http_certification::{HttpResponse, HttpUpdateResponse};

#[query]
fn http_request() -> HttpResponse<'static> {
    HttpResponse::builder().with_upgrade(true).build()
}

#[update]
fn http_request_update() -> HttpUpdateResponse<'static> {
    HttpResponse::builder()
        .with_status_code(StatusCode::IM_A_TEAPOT)
        .with_body(b"I'm a teapot")
        .build_update()
}

Motoko

The Motoko code is split into two functions: http_request and http_request_update. The http_request function is the entry point for the query call from the HTTP Gateway. It returns an HttpResponse with the upgrade field set to Some(true). The http_request_update function is the entry point for the update call from the HTTP Gateway. It returns an HttpUpdateResponse with a custom status code and body.

import Text "mo:base/Text";

actor Http {
  type HeaderField = (Text, Text);

  type HttpRequest = {
    method : Text;
    url : Text;
    headers : [HeaderField];
    body : Blob;
    certificate_version : ?Nat16;
  };

  type HttpUpdateRequest = {
    method : Text;
    url : Text;
    headers : [HeaderField];
    body : Blob;
  };

  type HttpResponse = {
    status_code : Nat16;
    headers : [HeaderField];
    body : Blob;
    upgrade : ?Bool;
  };

  type HttpUpdateResponse = {
    status_code : Nat16;
    headers : [HeaderField];
    body : Blob;
  };

  public query func http_request(_req: HttpRequest) : async HttpResponse {
    return {
      status_code = 200;
      headers = [];
      body = "";
      upgrade = ?true;
    };
  };

  public func http_request_update(_req: HttpUpdateRequest) : async HttpUpdateResponse {
    return {
      status_code = 418;
      headers = [];
      body = Text.encodeUtf8("I'm a teapot");
    };
  };
};

Testing the canister

This example uses a Rust canister called http_certification_upgrade_to_update_call_rust_backend or a Motoko canister called http_certification_upgrade_to_update_call_motoko_backend.

To test the canister, you can use dfx to start a local instance of the replica:

dfx start --background --clean

Testing the Rust canister

dfx deploy http_certification_upgrade_to_update_call_rust_backend

Make a request to the canister using curl:

curl -v http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_upgrade_to_update_call_rust_backend)

Testing the Motoko canister

dfx deploy http_certification_upgrade_to_update_call_motoko_backend

Make a request to the canister using curl:

curl -v http://localhost:$(dfx info webserver-port)?canisterId=$(dfx canister id http_certification_upgrade_to_update_call_motoko_backend)

Resources