Serving Static Assets with Rust WASM Workers Sites


This post is a follow-up to a question I asked on the CloudFlare Community forum.

Sure! After poking around, it seems that kv namespaces are the way to go. (The source for my project can be found here, btw).

About KV

Any files other than those used to compile the WASM executable won't be included in the package, which means that they have to be requested over database. Cloudflare's KV is a distributed key-value database that we can use for this purpose - here's the documentation. You'll first need to create a namespace. You can use the web interface, or the wrangler kv:create command.

Adding KV Namespace to Project

Each namespace has a unique id. To use the namespace in your project, you need to add this to your wrangler.toml (if you haven't already):

account_id = "..."
zone_id = "..."
kv-namespaces = [
    { binding = "Name", id = "..." },
    # ...
]

By the way: The tutorial seems to be incorrect, remove [site] ... from your wrangler.toml.

Your account id and your zone id can be found through Cloudflare's dashboard. You should also make sure your Cloudflare account is connected by running wrangler whoami - if you aren't authenticated, run wrangler config and follow the instructions.

Making the Namespace Accessible to WASM Worker

Our namespace is bound to our project, but our WASM Worker can't access it yet. To make it accessible, we need to do two things:

  1. Declare that our WASM worker can access the namespace.
  2. Define an interface that allows us to access the namespace from the worker.

Declaring Namespace Access

To declare that our worker can access the namespace, add the following to your worker/wasm_metadata.json:

{
    "body_part": "script",
    "bindings": [
        {
            "name": "wasm",
            "type": "wasm_module",
            "part": "wasmprogram"
        }
        {
          "name": "Name", // name of namespace
          "type": "kv_namespace",
          "namespace_id": "..."
        },
        // ... repeat the above block to add additional namespaces
    ]
}

Defining an Interface

Now that our worker can access the namespace, we need to write some Rust so we can read/write from/to the namespace. We can use extern to call kv javascript functions from our Rust WASM worker. Make a new Rust file, src/kv.rs, and add the following:

use wasm_bindgen::prelude::*;
use js_sys::Promise;
use wasm_bindgen_futures::JsFuture;

#[wasm_bindgen]
extern "C" {
    pub type Name; // name of namespace

    // notice these attributes!
    #[wasm_bindgen(static_method_of = Name)]
    pub fn get(key: &str, data_type: &str) -> Promise;

    #[wasm_bindgen(static_method_of = Name)]
    pub fn put(key: &str, val: &str) -> Promise;

    #[wasm_bindgen(static_method_of = Name)]
    pub fn delete(key: &str) -> Promise;
}

There are more functions availiable, but this allows us to get, store, and delete keys, which will realistically cover about 95% of use-cases.

Making it More Ergonomic

I also wrote a small helper function, value that awaits promises returned from the kv namespace. This is just for convinience:

// add to src/kv.rs
pub async fn value(promise: Promise) -> Option<JsValue> {
    JsFuture::from(promise).await.ok()
}

Then, reading/writing/deleting to/from the kv is as simple as:

pub async fn asset(name: &str) -> String {
    // again, Name is name of namespace
    kv::value(kv::Name::get(name, "text"))
        .await.unwrap().as_string().unwrap()
}

Which I personally find to be really archaic and annoying, which is why I've wrapped it with a function called asset.

Finally Serving Static Assets!

To serve these assets, we need to define a route endpoint in our Rust code. You should have some main function that's bound so it's externally accessible.

To serve static assets, we're going to check that the request is a GET request, check that the URL path starts with example.com/static/, extract the name of the asset (e.g. example.com/static/file.txtfile.txt), look up the asset in our KV namespace, make a response, then return it. This may seem needlessly complex (and to some degree, it is) but it's actually quite simple.

// in lib.rs
// ... imports hidden

/// Takes an event, handles it, and returns a promise containing a response.
#[wasm_bindgen]
pub async fn main(event: FetchEvent) -> Promise {
    // get the request
    let request = event.request();

    // extract the url
    let url = match Url::parse(&request.url()) {
        Ok(v) => v,
        Err(_) => return Promise::reject(&JsValue::from("Could not parse url")),
    };

    // get the URL path (e.g. www.example.com/<path>) 
    // and method (GET, POST, etc.)
    let path = url.path().to_lowercase();
    let method = request.method().to_lowercase();

    // split the path into a list of values
    // e.g. "static/file.txt" becomes ["static", "file.txt"]
    let route = path.split('/')
        .filter(|p| p != &"")
        .map(|p| p.to_string())
        .collect::<Vec<String>>();

    // I'm using an if statement here,
    // but if you have more paths to route against, 
    // a match statement is a better idea
    if route.iter().nth(0).unwrap() == "static" {
        let file = path.iter().nth(1);
        // using the previously defined asset function to read from the ns
        let content = asset(file);
        
       // ... snip ... building the response
       return Promise::resolve(&JsValue::from(response));
    }

    // ... do other things with the event as normal
}

Of course, the above logic doesn't have to be in your main function - you can abstract it out somewhat. (My project, for instance, has one function that handles routing, another that fetches static assets, and yet another that builds responses). I also strongly suggest you implement a more robust error handling mechanism than just unwrapping errors.

Uploading Static Assets

You can upload static assets to serve via Cloudflare's KV dashboard or in bulk from the command line using wrangler kv::bulk put.

If there's anything I glossed over or would like me to expand on, let me know. Most of the code above was taken from the repository for my website, so I would check it out. I hope this helps!

By the way: I would not recommend workers for serving static assets, it's a bit of a hassle. Workers is a bit rough around the edges in general, so unless you have a really good reason to use it, I recommend taking a more established route instead.*