cratefield-runtime-cloudflare 0.5.4

Cloudflare Workers runtime for the Cratefield harness (workers-rs)
Documentation

cratefield-runtime-cloudflare

Cloudflare Workers runtime for the Cratefield harness: maps Workers bindings to the harness ports and serves a Harness on #[event(fetch)] / #[event(scheduled)].

Usage

A venture's Worker is three lines:

use cratefield_core::Harness;
use cratefield_runtime_cloudflare::{serve, serve_scheduled, Cloudflare};
use std::sync::OnceLock;
use worker::{event, Context, Env, Request, Response};

static INSTANCE: OnceLock<(Harness, Cloudflare)> = OnceLock::new();

#[event(fetch)]
pub async fn fetch(req: Request, env: Env, ctx: Context) -> worker::Result<Response> {
    let (harness, runtime) = INSTANCE.get_or_init(build);
    serve(harness, runtime, req, env, ctx).await
}

#[event(scheduled)]
pub async fn scheduled(event: worker::ScheduledEvent, env: Env, ctx: worker::ScheduleContext) {
    let (harness, runtime) = INSTANCE.get_or_init(build);
    serve_scheduled(harness, runtime, event, env, ctx).await;
}

The runtime builder: Cloudflare::new().db("DB").kv("KV").rate_limiter("RATE_LIMITER") plus .mailer(...)/.captcha(...) with adapter instances. For per-key limits (per plan, per address — the binding above is one limit per namespace), wire .d1_rate_limiter("DB", policy) instead: a D1-backed fixed window per key whose policy maps each key to its cap. Ship the table it counts in by adding the RATE_LIMIT_COUNTERS_SQL statement to the venture's migrations/ directory.

wrangler.toml

name = "my-venture-api"
main = "build/worker/shim.mjs"
compatibility_date = "2026-09-01"

[build]
command = "cargo install -q worker-build && worker-build --release"

[observability]
enabled = true

[[d1_databases]]
binding = "DB"
database_name = "my-venture-db"
database_id = "<uuid>"
migrations_dir = "migrations"

# Optional bindings:
# [kv_namespaces] binding = "KV", id = "..."
# [unsafe.bindings] name = "RATE_LIMITER", type = "ratelimit"

Secrets (wrangler secret put, or .dev.vars locally — copy .dev.vars.example): HARNESS_SECRET (required for the Signer port, ≥ 32 bytes), HARNESS_SECRET_PREVIOUS (rotation), ADMIN_TOKEN, ENV.

wasm notes (workerd/miniflare, wrangler 4.x)

These are empirical facts recorded while building this crate; see PROGRESS.md issue #5 for the full debugging history:

  • The worker crate's http feature stays off: with it enabled, D1 writes hang the isolate. serve() therefore takes the native worker::Request (the fetch macro's FromRequest accepts it).
  • Request bodies are gated before they are resident (issue #440): a content-length over the route's ceiling is refused unread, and a body with no usable content-length is read through Request::stream() and cut off once the ceiling is passed. A request with no body at all (most GETs) is answered as empty without any read: 0.8.5's stream() sets body_used before discovering there is no body, after which bytes() only answers BodyUsed. Declared lengths within the ceiling are still buffered via Request::bytes(); every streaming bridge between worker::Body and axum hangs.
  • A route a module declares in streaming_routes() is served opt-in in streaming mode (issue #585). The request half turns Request::stream() into a RequestStream (a !Send worker ByteStream behind worker::send::SendWrapper, sound because an isolate is single-threaded — ADR 0002) and hands the router an empty body plus the handle, so nothing is buffered; a content-length over the route's own ceiling is still refused unread. The response half takes the ResponseStream a handler left in the response extensions and bridges it with worker::Response::from_stream — never to_bytes, which is capped at 1 MiB and for a stream would be the hang above. Every non-streaming response is still buffered under that 1 MiB ceiling.
  • wrangler dev only: refusing a body unread (issue #440) leaves the tail undrained, and the dev proxy's middleware-ensure-req-body-drained logs Failed to drain the unused request body. Error: Network connection lost. and breaks miniflare's ProxyWorker loopback slot, so the NEXT local request fails with a spurious 500 Network connection lost — it never reaches the isolate. Exactly one request is lost and the dev server recovers by itself; a client that overstates content-length and then sends fewer bytes may see the 413 withheld locally for the same reason. Dev-proxy artefact only: production workerd cancels unread request bodies natively (no ProxyWorker, no drain middleware). Verified wrangler 4.135.0 / workerd 1.20260918.1; known upstream as workers-sdk issue #4562.
  • Installing a tracing dispatcher (set_global_default/set_default) hangs the isolate, so install_tracing() is a no-op on wasm and runtime logs go through worker::console_log!/console_error! directly (rt_log!). The JSON-lines subscriber with field redaction is used on native runs.
  • The time crate needs its wasm-bindgen feature for now_utc() on wasm (pinned in the workspace Cargo.toml); without it every clock read panics (time not implemented on this platform).

MIT. Built in the open for Cratefield, a Factory Zero venture.