MCPcopy Index your code
hub / github.com/cloudflare/workers-rs

github.com/cloudflare/workers-rs @v0.8.5

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.8.5 ↗ · + Follow
1,416 symbols 3,300 edges 227 files 420 documented · 30%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

workers-rs crates.io docs.rs

Ergonomic Rust bindings to Cloudflare Workers environment. Write your entire worker in Rust!

Read the Notes and FAQ

Example Usage

use worker::*;

#[event(fetch)]
pub async fn main(mut req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
    console_log!(
        "{} {}, located at: {:?}, within: {}",
        req.method().to_string(),
        req.path(),
        req.cf().unwrap().coordinates().unwrap_or_default(),
        req.cf().unwrap().region().unwrap_or("unknown region".into())
    );

    if !matches!(req.method(), Method::Post) {
        return Response::error("Method Not Allowed", 405);
    }

    if let Some(file) = req.form_data().await?.get("file") {
        return match file {
            FormEntry::File(buf) => {
                Response::ok(&format!("size = {}", buf.bytes().await?.len()))
            }
            _ => Response::error("`file` part of POST form must be a file", 400),
        };
    }

    Response::error("Bad Request", 400)
}

Getting Started

The project uses wrangler for running and publishing your Worker.

Use cargo generate to start from a template:

cargo generate cloudflare/workers-rs

There are several templates to choose from. During generation you will be prompted to enable panic=unwind and abort recovery (see Panic Recovery below). You should see a new project layout with a src/lib.rs. Start there! Use any local or remote crates and modules (as long as they compile to the wasm32-unknown-unknown target).

Once you're ready to run your project, run your worker locally:

npx wrangler dev

Finally, go live:

# configure your routes, zones & more in your worker's `wrangler.toml` file
npx wrangler deploy

If you would like to have wrangler installed on your machine, see instructions in wrangler repository.

http Feature

worker 0.0.21 introduced an http feature flag which starts to replace custom types with widely used types from the http crate.

This makes it much easier to use crates which use these standard types such as axum and hyper.

This currently does a few things:

  1. Introduce Body, which implements http_body::Body and is a simple wrapper around web_sys::ReadableStream.
  2. The req argument when using the [event(fetch)] macro becomes http::Request<worker::Body>.
  3. The expected return type for the fetch handler is http::Response<B> where B can be any http_body::Body<Data=Bytes>.
  4. The argument for Fetcher::fetch_request is http::Request<worker::Body>.
  5. The return type of Fetcher::fetch_request is Result<http::Response<worker::Body>>.

The end result is being able to use frameworks like axum directly (see example):

pub async fn root() -> &'static str {
    "Hello Axum!"
}

fn router() -> Router {
    Router::new().route("/", get(root))
}

#[event(fetch)]
async fn fetch(
    req: HttpRequest,
    _env: Env,
    _ctx: Context,
) -> Result<http::Response<axum::body::Body>> {
    Ok(router().call(req).await?)
}

We also implement try_from between worker::Request and http::Request<worker::Body>, and between worker::Response and http::Response<worker::Body>. This allows you to convert your code incrementally if it is tightly coupled to the original types.

Or use the Router:

Parameterize routes and access the parameter values from within a handler. Each handler function takes a Request, and a RouteContext. The RouteContext has shared data, route params, Env bindings, and more.

use serde::{Deserialize, Serialize};
use worker::*;

#[event(fetch)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {

    // Create an instance of the Router, which can use parameters (/user/:name) or wildcard values
    // (/file/*pathname). Alternatively, use `Router::with_data(D)` and pass in arbitrary data for
    // routes to access and share using the `ctx.data()` method.
    let router = Router::new();

    // useful for JSON APIs
    #[derive(Deserialize, Serialize)]
    struct Account {
        id: u64,
        // ...
    }
    router
        .get_async("/account/:id", |_req, ctx| async move {
            if let Some(id) = ctx.param("id") {
                let accounts = ctx.kv("ACCOUNTS")?;
                return match accounts.get(id).json::<Account>().await? {
                    Some(account) => Response::from_json(&account),
                    None => Response::error("Not found", 404),
                };
            }

            Response::error("Bad Request", 400)
        })
        // handle files and fields from multipart/form-data requests
        .post_async("/upload", |mut req, _ctx| async move {
            let form = req.form_data().await?;
            if let Some(entry) = form.get("file") {
                match entry {
                    FormEntry::File(file) => {
                        let bytes = file.bytes().await?;
                    }
                    FormEntry::Field(_) => return Response::error("Bad Request", 400),
                }
                // ...

                if let Some(permissions) = form.get("permissions") {
                    // permissions == "a,b,c,d"
                }
                // or call `form.get_all("permissions")` if using multiple entries per field
            }

            Response::error("Bad Request", 400)
        })
        // read/write binary data
        .post_async("/echo-bytes", |mut req, _ctx| async move {
            let data = req.bytes().await?;
            if data.len() < 1024 {
                return Response::error("Bad Request", 400);
            }

            Response::from_bytes(data)
        })
        .run(req, env).await
}

Durable Object, KV, Secret, & Variable Bindings

All "bindings" to your script (Durable Object & KV Namespaces, Secrets, Variables and Version) are accessible from the env parameter provided to both the entrypoint (main in this example), and to the route handler callback (in the ctx argument), if you use the Router from the worker crate.

use worker::*;

#[event(fetch, respond_with_errors)]
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
    let router = Router::new();

    router
        .on_async("/durable", |_req, ctx| async move {
            let namespace = ctx.durable_object("CHATROOM")?;
            let stub = namespace.id_from_name("A")?.get_stub()?;
            // `fetch_with_str` requires a valid Url to make request to DO. But we can make one up!
            stub.fetch_with_str("http://fake_url.com/messages").await
        })
        .get("/secret", |_req, ctx| {
            Response::ok(ctx.secret("CF_API_TOKEN")?.to_string())
        })
        .get("/var", |_req, ctx| {
            Response::ok(ctx.var("BUILD_NUMBER")?.to_string())
        })
        .post_async("/kv", |_req, ctx| async move {
            let kv = ctx.kv("SOME_NAMESPACE")?;

            kv.put("key", "value")?.execute().await?;

            Response::empty()
        })
        .run(req, env).await
}

For more information about how to configure these bindings, see:

  • https://developers.cloudflare.com/workers/cli-wrangler/configuration#keys
  • https://developers.cloudflare.com/workers/learning/using-durable-objects#configuring-durable-object-bindings
  • https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/

Durable Objects

Define a Durable Object in Rust

To define a Durable Object using the worker crate you need to implement the DurableObject trait on your own struct. Additionally, the #[durable_object] attribute macro must be applied to the struct definition.

use worker::{durable_object, DurableObject, State, Env, Result, Request, Response};

#[durable_object]
pub struct Chatroom {
    users: Vec<User>,
    messages: Vec<Message>,
    state: State,
    env: Env, // access `Env` across requests, use inside `fetch`
}

impl DurableObject for Chatroom {
    fn new(state: State, env: Env) -> Self {
        Self {
            users: vec![],
            messages: vec![],
            state: state,
            env,
        }
    }

    async fn fetch(&self, _req: Request) -> Result<Response> {
        // do some work when a worker makes a request to this DO
        Response::ok(&format!("{} active users.", self.users.len()))
    }
}

You'll need to "migrate" your worker script when it's published so that it is aware of this new Durable Object, and include a binding in your wrangler.toml.

  • Include the Durable Object binding type in you wrangler.toml file:
# ...

[durable_objects]
bindings = [
  { name = "CHATROOM", class_name = "Chatroom" } # the `class_name` uses the Rust struct identifier name
]

[[migrations]]
tag = "v1" # Should be unique for each entry
new_classes = ["Chatroom"] # Array of new classes

SQLite Storage in Durable Objects

Durable Objects can use SQLite for persistent storage, providing a relational database interface. To enable SQLite storage, you need to use new_sqlite_classes in your migration and access the SQL storage through state.storage().sql().

use worker::{durable_object, DurableObject, State, Env, Result, Request, Response, SqlStorage};

#[durable_object]
pub struct SqlCounter {
    sql: SqlStorage,
}

impl DurableObject for SqlCounter {
    fn new(state: State, _env: Env) -> Self {
        let sql = state.storage().sql();
        // Create table if it does not exist
        sql.exec("CREATE TABLE IF NOT EXISTS counter(value INTEGER);", None)
            .expect("create table");
        Self { sql }
    }

    async fn fetch(&self, _req: Request) -> Result<Response> {
        #[derive(serde::Deserialize)]
        struct Row {
            value: i32,
        }

        // Read current value
        let rows: Vec<Row> = self
            .sql
            .exec("SELECT value FROM counter LIMIT 1;", None)?
            .to_array()?;
        let current = rows.get(0).map(|r| r.value).unwrap_or(0);
        let next = current + 1;

        // Update counter
        self.sql.exec("DELETE FROM counter;", None)?;
        self.sql
            .exec("INSERT INTO counter(value) VALUES (?);", vec![next.into()])?;

        Response::ok(format!("SQL counter is now {}", next))
    }
}

Configure your wrangler.toml to enable SQLite storage:

# ...

[durable_objects]
bindings = [
  { name = "SQL_COUNTER", class_name = "SqlCounter" }
]

[[migrations]]
tag = "v1" # Should be unique for each entry
new_sqlite_classes = ["SqlCounter"] # Use new_sqlite_classes for SQLite-enabled objects
  • For more information about migrating your Durable Object as it changes, see the docs here: https://developers.cloudflare.com/workers/learning/using-durable-objects#durable-object-migrations-in-wranglertoml

Queues

Enabling queues

As queues are in beta you need to enable the queue feature flag.

Enable it by adding it to the worker dependency in your Cargo.toml:

worker = {version = "...", features = ["queue"]}

Example worker consuming and producing messages:

use worker::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Clone, Deserialize)]
pub struct MyType {
    foo: String,
    bar: u32,
}

// Consume messages from a queue
#[event(queue)]
pub async fn main(message_batch: MessageBatch<MyType>, env: Env, _ctx: Context) -> Result<()> {
    // Get a queue with the binding 'my_queue'
    let my_queue = env.queue("my_queue")?;

    // Deserialize the message batch
    let messages = message_batch.messages()?;

    // Loop through the messages
    for message in messages {
        // Log the message and meta data
        console_log!(
            "Got message {:?}, with id {} and timestamp: {}",
            message.body(),
            message.id(),
            message.timestamp().to_string()
        );

        // Send the message body to the other queue
        my_queue.send(message.body()).await?;

        // Ack individual message
        message.ack();

        // Retry individual message
        message.retry();
    }

    // Retry all messages
    message_batch.retry_all();
    // Ack all messages
    message_batch.ack_all();
    Ok(())
}

You'll need to ensure you have the correct bindings in your wrangler.toml:

# ...
[[queues.consumers]]
queue = "myqueueotherqueue"
max_batch_size = 10
max_batch_timeout = 30


[[queues.producers]]
queue = "myqueue"
binding = "my_queue"

RPC Support

workers-rs has experimental support for Workers RPC. For now, this relies on JavaScript bindings and may require some manual usage of wasm-bindgen.

Not all features of RPC are supported yet (or have not been tested), including: - Function arguments and return values - Class instances - Stub forwarding

RPC Server

Writing an RPC server with workers-rs is relatively simple. Simply export methods using wasm-bindgen. These will be automatically detected by worker-build and made available to other Workers. See example.

RPC Client

Creating types and bindings for invoking another Worker's RPC methods is a bit more involved. You will need to write more complex wasm-bindgen bindings and some boilerplate to make interacting with the RPC methods more idiomatic. See [example](./examples/rpc-client/src/calculator.rs

Extension points exported contracts — how you extend this code

DurableObject (Interface)
Send is not needed [9 implementers]
worker/src/durable.rs
SendEmail (Interface)
* A binding that allows a Worker to send email messages. [2 implementers]
types/email.d.ts
BinaryDep (Interface)
(no doc) [3 implementers]
worker-build/src/binary.rs
HeadersExt (Interface)
(no doc) [1 implementers]
worker-sys/src/ext/headers.rs
Calculator (Interface)
(no doc) [1 implementers]
examples/rpc-client/src/calculator.rs
IntoResponse (Interface)
A trait used to represent any viable Response type that can be used in the Worker. The only requirement is that it be co [5 …
worker/src/response.rs
EmailSendResult (Interface)
* The returned data after sending an email
types/email.d.ts
GetBinary (Interface)
(no doc) [1 implementers]
worker-build/src/binary.rs

Core symbols most depended-on inside this repo

Ok
called by 307
worker/src/lib.rs
text
called by 146
worker/src/r2/mod.rs
test
called by 122
examples/tracing/src/lib.rs
clone
called by 70
worker/src/request.rs
get
called by 59
worker/src/cache.rs
error
called by 43
test/src/d1.rs
execute
called by 41
worker/src/kv/builder.rs
as_ref
called by 34
worker/src/ai.rs

Shape

Method 798
Function 351
Class 184
Enum 58
Interface 25

Languages

Rust97%
TypeScript3%

Modules by API surface

worker/src/durable.rs76 symbols
worker/src/queue.rs57 symbols
worker/src/r2/mod.rs48 symbols
worker/src/d1/mod.rs45 symbols
worker/src/response.rs44 symbols
worker/src/cf.rs44 symbols
worker/src/router.rs40 symbols
worker/src/socket.rs35 symbols
worker/src/bindings/email.rs35 symbols
worker-sys/src/types/email.rs35 symbols
worker/src/request_init.rs34 symbols
worker/src/websocket.rs31 symbols

For agents

$ claude mcp add workers-rs \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact