Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
3f61a28
Implement HTTP handlers / webhooks in Rust modules
gefjon Apr 8, 2026
8be80a2
Add a smoketest
gefjon Apr 9, 2026
ad560f1
fmt
gefjon Apr 9, 2026
690bf66
Reorganize imports to correct feature gates
gefjon Apr 10, 2026
7ac412a
Update bindings deps snapshot with `bytes`
gefjon Apr 10, 2026
77057cc
Update smoketest modules Cargo.lock for `bytes` dependency
gefjon Apr 10, 2026
977c1a0
Add additional smoketest with example from PR description
gefjon Apr 10, 2026
1a42332
Merge branch 'master' into phoebe/http-handlers-webhooks
gefjon Apr 10, 2026
ae3dfa3
fmt
gefjon Apr 10, 2026
f527c10
Regen C# moduledef to make `check-diff.sh` test pass
gefjon Apr 13, 2026
3df5ab2
Add bindings UI test for HTTP handlers
gefjon Apr 14, 2026
797f3dc
Remove unnecessary duplicated binding in macro output
gefjon Apr 14, 2026
151f224
Be more restrictive about characters allowed in HTTP routes
gefjon Apr 15, 2026
9892e9b
fmt
gefjon Apr 15, 2026
f976a00
clippy
gefjon Apr 15, 2026
c60824e
Merge origin/master into phoebe/http-handlers-webhooks
clockwork-labs-bot Apr 16, 2026
14e840e
Merge branch 'master' into phoebe/http-handlers-webhooks
gefjon Apr 17, 2026
7e20eda
Use updated Axum syntax for wildcards
gefjon Apr 17, 2026
999a7c3
Fix name collision with `const index` or `const name` colocated with …
gefjon Apr 21, 2026
847f989
Accept the empty route path; distinguish between empty and root
gefjon Apr 21, 2026
0788247
Silence warning on non-upper-case global emitted by `#[handler]`
gefjon Apr 21, 2026
c02f795
Expose full URI to handlers, incl. protocol and authority
gefjon Apr 22, 2026
0dde205
Simplify passing path into handler impl
gefjon Apr 22, 2026
4a4da89
Move `HandlerContext` to `spacetimedb::http::HandlerContext`.
gefjon Apr 24, 2026
86e6b6f
Add docs for HTTP handlers and router
gefjon Apr 24, 2026
b5e6e1a
Add smoketest that examples in docs work
gefjon Apr 24, 2026
6a98071
Merge remote-tracking branch 'origin/master' into phoebe/http-handler…
gefjon Apr 24, 2026
6c691ae
Axum 0.7 syntax for routes
gefjon Apr 24, 2026
df07ba9
Add forgotten file
gefjon Apr 24, 2026
163f6af
Correct (?) link
gefjon Apr 24, 2026
d296a17
Add a smoketest that inspects the request body
gefjon Apr 28, 2026
5f8e544
Expand comment re: unstable feature in CI
gefjon Apr 30, 2026
e832d2e
Separate request/response body from metadata at ABI level
gefjon Apr 30, 2026
7c54b94
Unify the "standard" and "extra" bytes sinks
gefjon Apr 30, 2026
af880fc
fmt
gefjon May 1, 2026
5999b64
Add doc comment to `Router`, amend doc comments on route register met…
gefjon May 1, 2026
5fab877
Add doc comments for `handler` and `router` macros
gefjon May 1, 2026
5c233e6
Respond to Mazdak's review
gefjon May 11, 2026
db472dc
Merge remote-tracking branch 'origin/master' into phoebe/http-handler…
gefjon May 11, 2026
032e23b
Merge remote-tracking branch 'origin/master' into phoebe/http-handler…
gefjon May 18, 2026
b258f95
fmt, clippy
gefjon May 18, 2026
a7c02cc
Put HTTP handler routes in `DatabaseRoutes`
gefjon May 19, 2026
cadc224
Add missing comma in merged autogen file
gefjon May 19, 2026
67c6890
C# HTTP handlers - Module Bindings (#5024)
JasonAtClockwork May 21, 2026
5f4180e
Merge remote-tracking branch 'origin/master' into phoebe/http-handler…
gefjon May 22, 2026
ac16512
fmt
gefjon May 22, 2026
6d4c323
Fix some merge mistakes
gefjon May 22, 2026
6b37954
And some more merge mistakes
gefjon May 22, 2026
c66379d
Regen module def for C#
JasonAtClockwork May 22, 2026
7f37ff6
TypeScript HTTP handlers (#4931)
gefjon May 22, 2026
f342524
Fix TS router path join for CodeQL alert
JasonAtClockwork May 22, 2026
6307a68
C++ HTTP handlers - Module Bindings (#5023)
JasonAtClockwork May 22, 2026
04771f6
Merge branch 'master' into phoebe/http-handlers-webhooks
gefjon May 28, 2026
ffb3fb0
Add basic troubleshooting guide
gefjon May 28, 2026
e84737c
Revert "Add basic troubleshooting guide"
gefjon May 28, 2026
5484877
Merge remote-tracking branch 'origin/master' into phoebe/http-handler…
gefjon May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 3 additions & 43 deletions crates/bindings-cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ target_sources(spacetimedb_cpp_library PRIVATE ${LIBRARY_SOURCES})

# Require C++20 for consumers of this library without forcing global flags
target_compile_features(spacetimedb_cpp_library PUBLIC cxx_std_20)
target_compile_definitions(spacetimedb_cpp_library PRIVATE SPACETIMEDB_UNSTABLE_FEATURES)

# Set include directories
target_include_directories(spacetimedb_cpp_library
Expand Down Expand Up @@ -60,46 +61,5 @@ if(PROJECT_IS_TOP_LEVEL)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
endif()

# ---- Tests ----
# Default: ON only when building this project directly; OFF when used via FetchContent/add_subdirectory
if(CMAKE_VERSION VERSION_LESS 3.21)
# Fallback heuristic for older CMake
set(_is_top_level FALSE)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(_is_top_level TRUE)
endif()
else()
set(_is_top_level ${PROJECT_IS_TOP_LEVEL})
endif()

option(BUILD_TESTS "Build the test suite" ${_is_top_level})

if(BUILD_TESTS AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
enable_testing()

# Add test executable
add_executable(test_bsatn tests/main.cpp tests/module_library_unit_tests.cpp)

# Link against the module library
target_link_libraries(test_bsatn PRIVATE spacetimedb_cpp_library)

# Set C++20 standard for tests
target_compile_features(test_bsatn PRIVATE cxx_std_20)

# Add test to CTest
add_test(NAME bsatn_tests COMMAND test_bsatn)

# Add verbose test variant
add_test(NAME bsatn_tests_verbose COMMAND test_bsatn -v)

# Set test properties
set_tests_properties(bsatn_tests PROPERTIES
TIMEOUT 30
LABELS "unit"
)

set_tests_properties(bsatn_tests_verbose PROPERTIES
TIMEOUT 30
LABELS "unit;verbose"
)
endif()
# Unit/compile/smoke test harnesses live under `tests/` as standalone runners
# rather than being built through the top-level library CMake target.
5 changes: 5 additions & 0 deletions crates/bindings-cpp/include/spacetimedb.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@

// Procedure context and macros
#include "spacetimedb/procedure_macros.h"
#ifdef SPACETIMEDB_UNSTABLE_FEATURES
#include "spacetimedb/handler_context.h"
#include "spacetimedb/router.h"
#include "spacetimedb/http_handler_macros.h"
#endif

// =============================================================================
// VIEW SYSTEM
Expand Down
31 changes: 31 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/abi/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,37 @@ int16_t __call_reducer__(
BytesSource args,
BytesSink error);

STDB_EXPORT(__call_view__)
int16_t __call_view__(
uint32_t id,
uint64_t sender_0, uint64_t sender_1, uint64_t sender_2, uint64_t sender_3,
BytesSource args,
BytesSink result);

STDB_EXPORT(__call_view_anon__)
int16_t __call_view_anon__(
uint32_t id,
BytesSource args,
BytesSink result);

STDB_EXPORT(__call_procedure__)
int16_t __call_procedure__(
uint32_t id,
uint64_t sender_0, uint64_t sender_1, uint64_t sender_2, uint64_t sender_3,
uint64_t conn_id_0, uint64_t conn_id_1,
uint64_t timestamp_microseconds,
BytesSource args_source,
BytesSink result_sink);

STDB_EXPORT(__call_http_handler__)
int16_t __call_http_handler__(
uint32_t id,
uint64_t timestamp_microseconds,
BytesSource request_source,
BytesSource request_body_source,
BytesSink response_sink,
BytesSink response_body_sink);

// ========================================================================
// WASI SHIMS
// ========================================================================
Expand Down
92 changes: 92 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/handler_context.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#ifndef SPACETIMEDB_HANDLER_CONTEXT_H
#define SPACETIMEDB_HANDLER_CONTEXT_H

#ifndef SPACETIMEDB_UNSTABLE_FEATURES
#error "spacetimedb/handler_context.h requires SPACETIMEDB_UNSTABLE_FEATURES to be enabled"
#endif

#include <spacetimedb/abi/FFI.h>
#include <spacetimedb/bsatn/timestamp.h>
#include <spacetimedb/bsatn/uuid.h>
#include <spacetimedb/http.h>
#include <spacetimedb/internal/tx_execution.h>
#include <spacetimedb/random.h>
#include <spacetimedb/tx_context.h>
#include <array>
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <type_traits>

namespace SpacetimeDB {

struct HandlerContext {
Timestamp timestamp;
HttpClient http;

private:
mutable std::shared_ptr<StdbRng> rng_instance;
mutable uint32_t counter_uuid_ = 0;

public:
HandlerContext() = default;
explicit HandlerContext(Timestamp t) : timestamp(t) {}

Identity identity() const {
std::array<uint8_t, 32> id_bytes;
::identity(id_bytes.data());
return Identity(id_bytes);
}

StdbRng& rng() const {
if (!rng_instance) {
rng_instance = std::make_shared<StdbRng>(timestamp);
}
return *rng_instance;
}

Uuid new_uuid_v4() const {
std::array<uint8_t, 16> random_bytes;
rng().fill_bytes(random_bytes.data(), random_bytes.size());
return Uuid::from_random_bytes_v4(random_bytes);
}

Uuid new_uuid_v7() const {
std::array<uint8_t, 4> random_bytes;
rng().fill_bytes(random_bytes.data(), random_bytes.size());
return Uuid::from_counter_v7(counter_uuid_, timestamp, random_bytes);
}

#ifdef SPACETIMEDB_UNSTABLE_FEATURES
template<typename Func>
auto with_tx(Func&& body) -> decltype(body(std::declval<TxContext&>())) {
auto make_reducer_ctx = [](Timestamp tx_timestamp) {
return ReducerContext(
Identity{},
std::nullopt,
tx_timestamp,
AuthCtx::internal()
);
};
return Internal::with_tx(make_reducer_ctx, body);
}

template<typename Func>
auto try_with_tx(Func&& body) -> decltype(body(std::declval<TxContext&>())) {
auto make_reducer_ctx = [](Timestamp tx_timestamp) {
return ReducerContext(
Identity{},
std::nullopt,
tx_timestamp,
AuthCtx::internal()
);
};
return Internal::try_with_tx(make_reducer_ctx, body);
}
#endif
};

} // namespace SpacetimeDB

#endif // SPACETIMEDB_HANDLER_CONTEXT_H
10 changes: 8 additions & 2 deletions crates/bindings-cpp/include/spacetimedb/http.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

#pragma once

#ifndef SPACETIMEDB_UNSTABLE_FEATURES
#error "spacetimedb/http.h requires SPACETIMEDB_UNSTABLE_FEATURES to be enabled"
#endif

#include <string>
#include <vector>
#include <optional>
Expand Down Expand Up @@ -312,8 +316,10 @@ class HttpClient {

} // namespace SpacetimeDB

// Include implementation after class definition to avoid circular dependencies
#ifdef SPACETIMEDB_UNSTABLE_FEATURES
// Include implementation dependencies after class definition to avoid circular dependencies
#if defined(SPACETIMEDB_UNSTABLE_FEATURES) && !defined(SPACETIMEDB_HTTP_CONVERT_H)
#include "spacetimedb/logger.h"
#include "spacetimedb/http_convert.h"
#include "spacetimedb/http_client_impl.h"
#endif

Expand Down
15 changes: 5 additions & 10 deletions crates/bindings-cpp/include/spacetimedb/http_client_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#include "spacetimedb/http_convert.h"
#include "spacetimedb/abi/abi.h"
#include "spacetimedb/bsatn/bsatn.h"
#include "spacetimedb/internal/Module.h"
#include "spacetimedb/internal/runtime_registration.h"

namespace SpacetimeDB {

Expand All @@ -23,9 +23,9 @@ inline Outcome<HttpResponse> HttpClient::SendImpl(const HttpRequest& request) {
// Prepare body bytes
const std::vector<uint8_t>& body_bytes = request.body.bytes;

// Call host function
// Note: For empty body, we need to pass a valid pointer, not null
const uint8_t* body_ptr = body_bytes.empty() ? reinterpret_cast<const uint8_t*>("") : body_bytes.data();
// The host ABI requires a non-null, in-bounds body pointer even when body_len == 0.
static const uint8_t empty_sentinel = 0;
const uint8_t* body_ptr = body_bytes.empty() ? &empty_sentinel : body_bytes.data();

BytesSource out[2] = {BytesSource{0}, BytesSource{0}};
Status status = procedure_http_request(
Expand All @@ -40,15 +40,11 @@ inline Outcome<HttpResponse> HttpClient::SendImpl(const HttpRequest& request) {
if (status.inner == 21) {
// Read error message from out[0]
std::vector<uint8_t> error_bytes = Internal::ConsumeBytes(out[0]);

LOG_INFO("HTTP: Error bytes: " + std::to_string(error_bytes.size()));


// Decode BSATN string
bsatn::Reader reader(error_bytes.data(), error_bytes.size());
std::string error_message = bsatn::deserialize<std::string>(reader);

LOG_INFO("HTTP: Error message: " + error_message);

return Err<HttpResponse>(std::move(error_message));
}

Expand All @@ -57,7 +53,6 @@ inline Outcome<HttpResponse> HttpClient::SendImpl(const HttpRequest& request) {
return Err<HttpResponse>("HTTP requests are blocked inside transactions. Call HTTP before with_tx() or try_with_tx().");
}

LOG_INFO("HTTP: Unknown error code: " + std::to_string(status.inner));
return Err<HttpResponse>("HTTP request failed with status code: " + std::to_string(status.inner));
}

Expand Down
15 changes: 15 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/http_convert.h
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ inline HttpRequest from_wire(const wire::HttpRequest& request) {
return result;
}

inline HttpRequest from_wire(const wire::HttpRequest& request, std::vector<uint8_t> body) {
HttpRequest result = from_wire(request);
result.body.bytes = std::move(body);
return result;
}

// ==================== HttpResponse Conversions ====================

/**
Expand Down Expand Up @@ -268,7 +274,16 @@ inline HttpResponse from_wire(const wire::HttpResponse& response) {
return result;
}

inline std::pair<wire::HttpResponse, std::vector<uint8_t>> to_wire_split(const HttpResponse& response) {
return {to_wire(response), response.body.bytes};
}

} // namespace convert
} // namespace SpacetimeDB

#ifdef SPACETIMEDB_UNSTABLE_FEATURES
#include "spacetimedb/logger.h"
#include "spacetimedb/http_client_impl.h"
#endif

#endif // SPACETIMEDB_HTTP_CONVERT_H
61 changes: 61 additions & 0 deletions crates/bindings-cpp/include/spacetimedb/http_handler_macros.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#pragma once

#ifndef SPACETIMEDB_UNSTABLE_FEATURES
#error "spacetimedb/http_handler_macros.h requires SPACETIMEDB_UNSTABLE_FEATURES to be enabled"
#endif

#include "spacetimedb/handler_context.h"
#include "spacetimedb/http.h"
#include "spacetimedb/internal/runtime_registration.h"
#include "spacetimedb/internal/template_utils.h"
#include "spacetimedb/internal/v10_builder.h"
#include "spacetimedb/macros.h"
#include "spacetimedb/router.h"

namespace SpacetimeDB::Internal {

template<typename Func>
inline void RegisterHttpHandlerMacro(const char* handler_name, Func func) {
using traits = function_traits<Func>;
static_assert(traits::arity == 2, "HTTP handlers must take exactly two arguments");
using ContextType = typename traits::template arg_t<0>;
using RequestType = typename traits::template arg_t<1>;
using ReturnType = typename traits::result_type;
static_assert(std::is_same_v<ContextType, HandlerContext>, "First parameter of HTTP handler must be HandlerContext");
static_assert(std::is_same_v<RequestType, HttpRequest>, "Second parameter of HTTP handler must be HttpRequest");
static_assert(std::is_same_v<ReturnType, HttpResponse>, "HTTP handlers must return HttpResponse");

std::function<HttpResponse(HandlerContext&, HttpRequest)> handler =
[func](HandlerContext& ctx, HttpRequest request) -> HttpResponse {
return func(ctx, std::move(request));
};
RegisterHttpHandlerHandler(handler_name, func, std::move(handler));
getV10Builder().RegisterHttpHandlerDef(handler_name);
}

} // namespace SpacetimeDB::Internal

#define SPACETIMEDB_HTTP_HANDLER(handler_name, ctx_param, request_param) \
SpacetimeDB::HttpResponse handler_name(ctx_param, request_param); \
__attribute__((export_name("__preinit__60_http_handler_" #handler_name))) \
extern "C" void CONCAT(_spacetimedb_preinit_register_http_handler_, handler_name)() { \
::SpacetimeDB::Internal::RegisterHttpHandlerMacro(#handler_name, handler_name); \
} \
SpacetimeDB::HttpResponse handler_name(ctx_param, request_param)

#define SPACETIMEDB_HTTP_HANDLER_NAMED(handler_name, canonical_name, ctx_param, request_param) \
SpacetimeDB::HttpResponse handler_name(ctx_param, request_param); \
__attribute__((export_name("__preinit__60_http_handler_" #handler_name))) \
extern "C" void CONCAT(_spacetimedb_preinit_register_http_handler_, handler_name)() { \
::SpacetimeDB::Internal::RegisterHttpHandlerMacro(#handler_name, handler_name); \
SpacetimeDB::Module::RegisterExplicitFunctionName(#handler_name, canonical_name); \
} \
SpacetimeDB::HttpResponse handler_name(ctx_param, request_param)

#define SPACETIMEDB_HTTP_ROUTER(router_name) \
SpacetimeDB::Router router_name(); \
__attribute__((export_name("__preinit__61_http_router_" #router_name))) \
extern "C" void CONCAT(_spacetimedb_preinit_register_http_router_, router_name)() { \
::SpacetimeDB::Internal::getV10Builder().RegisterHttpRouter(router_name()); \
} \
SpacetimeDB::Router router_name()
Loading
Loading