Skip to content

Implement HTTP handlers / webhooks in Rust modules#4636

Merged
gefjon merged 56 commits into
masterfrom
phoebe/http-handlers-webhooks
May 29, 2026
Merged

Implement HTTP handlers / webhooks in Rust modules#4636
gefjon merged 56 commits into
masterfrom
phoebe/http-handlers-webhooks

Conversation

@gefjon
Copy link
Copy Markdown
Contributor

@gefjon gefjon commented Mar 13, 2026

Description of Changes

Adds support to Rust modules and the SpacetimeDB host for defining HTTP handlers and registering them to routes.

User-facing API

In a Rust module, users can annotate functions with the new macro #[spacetimedb::http::handler]. A function annotated this way must accept exactly two arguments, of types &mut spacetimedb::http::HandlerContext and spacetimedb::http::Request (which is a type alias for http::Request<spacetimedb::http::Body>. It must also return spacetimedb::http::Response (which is a type alias for http::Response<spacetimedb::http::Body>).

Once the user has defined an HTTP handler, they can register it to a route by annotating a function with #[spacetimedb::http::router]. Such a function must take no arguments and return a spacetimedb::http::Router. (The original design put this annotation on a static variable rather than a function, but that turned out to be undesirable because it required that constructing a Router be const.) Router exposes various methods for registering handlers to routes.

All of a database's user-defined routes are exposed under /v1/database/:name_or_identity/route/{*path}.

Example

See the new smoketest for a more exhaustive example.

A simpler example, which stores arbitrary byte data in a table via a POST request, returns an ID, and then retrieves that same data via a GET request with a query parameter:

#[spacetimedb::table(accessor = data)]
struct Data {
    #[primary_key]
    #[auto_inc]
    id: u64,
    body: Vec<u8>,
}

#[spacetimedb::http::handler]
fn insert(ctx: &mut HandlerContext, request: Request) -> Response {
    let body: Vec<u8> = request.into_body().into_bytes().into();
    let id = ctx.with_tx(|tx| tx.db.data().insert(Data { id: 0, body: body.clone() }).id);
    Response::new(Body::from_bytes(format!("{id}")))
}

#[spacetimedb::http::handler]
fn retrieve(ctx: &mut HandlerContext, request: Request) -> Response {
    let id = request
        .uri()
        .query()
        .and_then(|query| query.strip_prefix("id="))
        .and_then(|id| u64::from_str(id).ok())
        .unwrap();
    let body = ctx.with_tx(|tx| tx.db.data().id().find(id).map(|data| data.body));
    if let Some(body) = body {
        Response::new(Body::from_bytes(body))
    } else {
        Response::builder().status(404).body(Body::empty()).unwrap()
    }
}

#[spacetimedb::http::router]
fn router() -> Router {
    Router::new().post("/insert", insert).get("/retrieve", retrieve)
}

Design and implementation notes

  • As mentioned above, the router is registered via a function, not a static or const item. This is because static or const initializers must be const, and it turns out to be a pain to make all of the Router constructors be const fns.
  • The #[handler] macro clobbers the original function name with a const variable of type HttpHandler. This is unfortunate, but AFAICT necessary, 'cause we need to pass the string identifier for the handler to the Router, not the function pointer, and Rust allows no (stable and reliable) way to get a unique string identifier out of a function item/value, nor to attach data or implement traits for function items/values. The alternative(s) would involve changing the signature of the Router methods to have uglier and more complex callsites, e.g. like .get("/retrieve", retrieve::handler()), .get("/retrieve", handler!(retrieve)) or .get::<retrieve>("/retrieve"). I believe that registering handlers will be much more common than calling their functions, so I've chosen to make it so that registering them gets the convenient syntax, even though the inability to call them directly will be somewhat surprising.
  • I haven't wired up energy handling or timing metrics for handler execution to anywhere. Procedures are still in the same boat.
  • HTTP requests to user-defined routes bypass the usual SpacetimeDB auth middleware, meaning that the host does not validate (or inspect in any way) Authorization headers in requests before invoking the user-defined handler. This is required to allow arbitrary user-programmable handling of Authorization headers, including those in formations which SpacetimeDB would reject. As a result of this, HandlerContext doesn't expose a sender or sender_connection_id.
  • HTTP route paths may consist only of a very restrictive set of characters. I've chosen this set to keep our options open in the future to add additional syntax to routes, like for registering wildcard segments and path parameters:
    • ASCII digits.
    • ASCII letters.
    • -_~/.
  • The internal data structure that represents a Router is currently a Vec<Route>, meaning that resolving a request to a route is O(num_routes). Registering a route checks against each previous route for uniqueness, meaning that constructing a router is O(num_routes ^ 2). There are TODO comments to use a trie, but I think this can wait, as I expect most databases to register few routes.
  • Commit 999a7c3 contains a fix to a mostly-unrelated bug where a few bindings introduced by the SATS derive macros were unhygienic and not in a reserved namespace, leading to name conflicts. I discovered this 'cause I tried writing an HTTP handler named index to serve the index/root of a website and it broke.

Still TODO

  • Resolve various TODO comments in the diff.
  • Documentation.
  • C# bindings support.
  • C++ bindings support.
  • V8 host support.
  • TypeScript bindings support.

API and ABI breaking changes

New APIs, currently flagged as unstable, which will eventually need stability guarantees. No (intentional) breaking changes, or changes to existing APIs at all.

Expected complexity level and risk

3? Changes to our HTTP routing to support the user-defined routes.

Testing

  • New smoketest of the behavior!
  • I dunno, maybe try hosting a simple webpage and see how it works?
  • Build a test app with Stripe integration.

Codex-assisted changes to implement HTTP handlers / webhooks, based on proposal discussed elsewhere.
I've done a reasonably thorough review of these changes for code quality,
though they remain largely untested as of this commit.
I've made a few minor changes to the LLM's output,
and left TODO comments anywhere I feel more substative changes will be necessary.

Most notably, we'll need more testing, documentation,
and better detection of suspicious-or-invalid path components.
TypeScript, C# and C++ support is also not included, as per title.
There are also notes that this initial implementation uses `Vec<Route>` for its router,
meaning route matching is O(num_routes), and router construction (with error checking) is O(num_routes ^ 2).
I don't expect this to matter much in the short term,
as I expect modules to define a pretty small number of routes.
@gefjon gefjon force-pushed the phoebe/http-handlers-webhooks branch from fbbe5b6 to 3f61a28 Compare April 8, 2026 17:31
@gefjon gefjon changed the title HTTP routes / webhooks Implement HTTP handlers / webhooks in Rust modules Apr 8, 2026
gefjon added 6 commits April 9, 2026 14:43
Added a smoketest that publishes a module with some routes,
then makes requests against those routes and makes some simple assertions about the responses.

This revealed a bug introduced by the previous commit in the `/v1/database PUT` route,
which was incorrectly not getting the `anon_auth_middleware` applied.
@gefjon gefjon marked this pull request as ready for review April 10, 2026 16:57
gefjon added 2 commits April 13, 2026 12:38
And relatedly, make the bindings tests run with `--feature unstable` in CI.

I was sadly unable to find a better construction to emit tyck code from the macros
than the existing one.

I also did a drive-by fix for an invalid doctest on `HttpClient`.
@gefjon gefjon requested review from bfops and jdetter as code owners April 14, 2026 16:38
gefjon added 4 commits April 14, 2026 12:46
Restrict HTTP routes to allow only characters permitted by a new function,
`spacetimedb_lib::http::character_is_acceptable_for_route_path`,
which currently is restricted to ASCII lowercase, ASCII digit and `-_~/`.
(Slash is allowed because it's the path segment separator character.)

This is checked both when constructing the `Router` and during `ModuleDef` validation.
Copy link
Copy Markdown
Collaborator

@bfops bfops left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my code-owned files LGTM:

tools/ci/src/main.rs

@gefjon
Copy link
Copy Markdown
Contributor Author

gefjon commented Apr 17, 2026

I had Codex vibe-code up a simple static webpage hosted entirely in a SpacetimeDB module. This was easy enough and mostly totally worked, but we ran into a few silly issues which I'll work to fix over the next few days. In particular:

  • A name conflict between having an HTTP handler named index and a table. This would, I believe, manifest if any module code defined a const named index in the same file as a #[table] macro invocation, but that's unlikely to happen in the wild due to consts usually having all-caps names.
  • The root route was accessible at /v1/database/:name/route, but not at /v1/database/:name/route/ (note the trailing slash). This was especially unintuitive as the root route is registered as "/".
  • The request path visible inside the handler was different from what was expected. The AI said:

    Externally the page was served at /v1/database/{db}/route, but inside the handler the incoming path looked like /{db}/route..., [...]

  • We get warnings from the #[handler] macro output warning about lowercase constants that should be uppercase. This just needs an #[allow].

…s-webhooks

Resolved conflicts in:
  crates/core/src/host/module_host.rs
  crates/core/src/host/wasm_common/module_host_actor.rs
gefjon added 4 commits May 18, 2026 10:34
…s-webhooks

Resolved conflicts related to the new module def section for mounts:
	crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs
	crates/lib/src/db/raw_def/v10.rs
	crates/schema/src/def.rs
	crates/schema/src/def/validate/v10.rs
So that SpacetimeDB-cloud can apply proxy middleware.
# Description of Changes

Adding C# HTTP handlers based on #4636

- adds the C# handler/router API `[SpacetimeDB.HttpHandler]`,
`[SpacetimeDB.HttpRouter]`
- wires C# HTTP handlers into module definition/build/runtime
registration
- mirrors the Rust/TypeScript HTTP smoketests and adds C# docs coverage
- updates the HTTP handlers docs with C# examples
- refactored ProcedureContext to allow for a central location for
WithTx/TryWithTx to support HandlerContext
- routes use a generated `Handlers.*` tokens to avoid raw strings

# API and ABI breaking changes

Adds new APIs for the HTTP handler and should not be breaking

# Expected complexity level and risk

3 - this hit the binding, module registration, and had a decent refactor
for ProcedureContext

# Testing

- [x] Expanded `crates/smoketests/tests/smoketests/http_routes.rs` with
C# mirrors of the Rust HTTP
route tests

I also did some manual testing with a throw away project, and will be
adding to the `module-test` after all languages are caught up on HTTP
handlers.
gefjon and others added 6 commits May 22, 2026 13:07
…s-webhooks

Resolved conflicts due to reverting RawModuleDefV10 mounts:
	crates/bindings-csharp/Runtime/Internal/Autogen/RawModuleDefV10Section.g.cs
	crates/lib/src/db/raw_def/v10.rs
	crates/schema/src/def.rs
	crates/schema/src/def/validate/v10.rs
Based on #4636 .

# Description of Changes

This commit adds host support for registering HTTP handlers in V8
modules, and a minimal draft of TypeScript bindings support for the
same. The TypeScript bindings support is fully vibe-coded and
unreviewed, and is present only to allow a new smoketest, which is added
to the `http_routes` suite. The host changes were also AI-assisted, but
I reviewed and polished them.

# API and ABI breaking changes

Adds new TypeScript "ABI." Also adds a new API.

# Expected complexity level and risk

2: pretty simple extensions to TypeScript execution, which largely
mirror existing `call_procedure` machinery.

# Testing

- [x] New smoketest.

---------

Co-authored-by: Jason Larabie <jason@clockworklabs.io>
Comment thread crates/bindings-typescript/src/server/http_handlers.ts Fixed
JasonAtClockwork and others added 6 commits May 22, 2026 15:00
# Description of Changes

Adding C++ HTTP handlers based on #4636

- adds the C++ handler/router API `SPACETIMEDB_HTTP_HANDLER()`,
`SPACETIMEDB_HTTP_ROUTER()`
- wires C++ HTTP handlers into module definition/build/runtime
registration
- mirrors the Rust/TypeScript HTTP smoketests and docs coverage
- update to the the HTTP handlers docs examples

# API and ABI breaking changes

New APIs for http handler and router behind
`SPACETIMEDB_UNSTABLE_FEATURES` and should not be breaking.

# Expected complexity level and risk

3 - this touches the C++ binding surface, runtime/module registration,
and the smoketest/docs paths, so there is more churn than I’d like, but
it is still fairly contained to the C++ HTTP work.

# Testing

- [x] Expanded the C++ compile-surface tests
- [x] Added C++ unit coverage for HTTP conversion helpers
- [x] Expanded `crates/smoketests/tests/smoketests/http_routes.rs` with
C++ mirrors of the Rust HTTP
route tests
- [x] Added a C++ docs-example smoketest for
`docs/docs/00200-core-concepts/00200-functions/00600-HTTP-
handlers.md`

I also did some manual testing with a throw away project

---------

Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Resolved conflicts re: Wasmtime module instantiation in:
	crates/core/src/host/wasm_common/module_host_actor.rs
	crates/core/src/host/wasmtime/wasmtime_module.rs
This reverts commit ffb3fb0.
This was supposed to be on a different branch. Oops.
@gefjon gefjon added this pull request to the merge queue May 29, 2026
Merged via the queue into master with commit 5c04860 May 29, 2026
74 of 76 checks passed
bfops added a commit that referenced this pull request Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants