Files
Tyler Cloutier d5c75d944d Ungate procedures from the unstable feature (#5164)
# Description of Changes

Graduate **procedures** (and the outgoing HTTP client used from
procedures) out of the `unstable` feature gate /
`SPACETIMEDB_UNSTABLE_FEATURES` across all module libraries. The
`procedure` macro, `ProcedureContext`, `with_tx`/`try_with_tx`,
scheduled procedures, and `ctx.http` are now available without opting
into `unstable`.

**Still gated** (unchanged): HTTP handlers/webhooks, views, RLS /
`client_visibility_filter`, and immediate-scheduling
(`volatile_nonatomic_schedule_immediate`).

Per library:
- **`bindings-sys`**: ungate the `procedure` host-call module + raw ABI
imports (`procedure_start/commit/abort_mut_tx`,
`procedure_http_request`) and `call_no_ret`. Scheduling ABI stays gated.
- **`bindings` (Rust)**: ungate the `procedure` macro,
`ProcedureContext`, `TxContext`/`with_tx`/`try_with_tx`,
`db_read_only`/`get_read_only`, the procedure traits,
`register_procedure`, `__call_procedure__`, and procedure RNG. The
`http` module (previously gated as a unit) now has fine-grained gating
so the outgoing `HttpClient` is exposed while
`HandlerContext`/`Router`/handler macros stay gated.
- **`bindings-csharp`**: drop `[Experimental("STDB_UNSTABLE")]` from
`ProcedureContext.WithTx/TryWithTx` (runtime + codegen) and the
generated `ProcedureTxContext`; FFI snapshots regenerated. Handler
context members + RLS attribute stay gated.
- **`bindings-cpp`**: ungate the procedure ABI (`abi.h`/`FFI.h`),
`tx_execution.h`, the outgoing HTTP client (`http.h`/`http_convert.h`),
and `procedure_context.h`.
`handler_context.h`/`router.h`/`http_handler_macros.h` still `#error`
without `SPACETIMEDB_UNSTABLE_FEATURES`.
- **docs**: remove the procedures beta notices (HTTP handlers stay
marked beta for a later release); regenerate `static/llms.md`.
- **`sdk-test-procedure`**: no longer needs `features = ["unstable"]`.

# API and ABI breaking changes

Not breaking. This is purely additive to the stable surface: procedures
become available **without** the `unstable` feature, while modules that
already opt into `unstable` are unaffected. The wasm host-ABI imports
were already implemented host-side and merely gated module-side, so
there is no ABI version change. (No breaking-change label needed.)

# Expected complexity level and risk

**3/5.** The diff itself is mostly removing
`#[cfg]`/`#ifdef`/`[Experimental]` guards, but the gate bundled
procedures + outgoing HTTP + handlers + the ABI crate together, so the
work was in *separating* procedures from the features that stay gated.
Areas reviewers may want to scrutinize:
- The Rust `http` module went from "gated as a whole" to fine-grained
per-item gating; the outgoing `HttpClient` is exposed while handler
types/macros stay behind `unstable`.
- The `unstable` cut reaches the ABI crate (`bindings-sys`), which is
the only way procedures can compile without the feature.
- C++ separation of the outgoing HTTP client from HTTP handlers across
several headers.

# Testing

- [x] Rust: `cargo check -p spacetimedb` passes in default, `--features
unstable`, and `--no-default-features --features unstable`.
- [x] Rust end-to-end: `sdk-test-procedure` (uses procedures,
`with_tx`/`try_with_tx`, `ctx.http.get`, `new_uuid_v7`, scheduled
procedures) builds **without** `unstable`.
- [x] C#: Codegen + Runtime build; `Codegen.Tests` pass (6/6) after FFI
snapshot regen.
- [x] C++: host syntax-check confirms procedures + `ctx.http.send()`
compile **without** the flag, full headers compile **with** it, and
`handler_context.h` still `#error`s without it. (Not built for the real
wasm/emscripten target locally — relying on CI.)
- [x] `cargo ci lint` components reproduced locally: rustfmt, clippy
(`-D warnings`, default + `unstable`), csharpier, and `cargo doc --deny
warnings`.
- [ ] Reviewer: confirm the wasm bindings + C#/C++ test suites pass in
CI (especially the C++ wasm build, which couldn't run locally).

---------

Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
2026-06-05 16:09:36 +00:00

288 lines
9.2 KiB
C++

#ifndef SPACETIMEDB_HTTP_CONVERT_H
#define SPACETIMEDB_HTTP_CONVERT_H
#pragma once
#include "spacetimedb/http.h"
#include "spacetimedb/http_wire.h"
/**
* @file http_convert.h
* @brief Conversion functions between user-facing HTTP types and BSATN wire types
*
* This module provides bidirectional conversion between:
* - User-facing types (http.h): HttpMethod, HttpVersion, HttpHeader, etc.
* - Wire types (http_wire.h): wire::HttpMethod, wire::HttpVersion, etc.
*
* The wire types are used for BSATN serialization when communicating with the
* SpacetimeDB host. User code should never interact with wire types directly.
*
* Note: The `is_sensitive` flag from HttpHeader is NOT preserved in the wire format.
* It's a local-only hint and is lost during conversion to wire format. When converting
* back from wire format, all headers are marked as non-sensitive.
*
* @ingroup sdk_internal
*/
namespace SpacetimeDB {
namespace convert {
// ==================== HttpMethod Conversions ====================
/**
* @brief Convert user-facing HttpMethod to wire format
*
* Standard methods (GET, POST, etc.) map to unit enum variants.
* Non-standard methods are stored in the Extension variant.
*/
inline wire::HttpMethod to_wire(const HttpMethod& method) {
wire::HttpMethod result;
// Check for standard methods
if (method.value == "GET") {
result.tag = wire::HttpMethod::Tag::Get;
} else if (method.value == "HEAD") {
result.tag = wire::HttpMethod::Tag::Head;
} else if (method.value == "POST") {
result.tag = wire::HttpMethod::Tag::Post;
} else if (method.value == "PUT") {
result.tag = wire::HttpMethod::Tag::Put;
} else if (method.value == "DELETE") {
result.tag = wire::HttpMethod::Tag::Delete;
} else if (method.value == "CONNECT") {
result.tag = wire::HttpMethod::Tag::Connect;
} else if (method.value == "OPTIONS") {
result.tag = wire::HttpMethod::Tag::Options;
} else if (method.value == "TRACE") {
result.tag = wire::HttpMethod::Tag::Trace;
} else if (method.value == "PATCH") {
result.tag = wire::HttpMethod::Tag::Patch;
} else {
// Non-standard method - store in Extension variant
result.tag = wire::HttpMethod::Tag::Extension;
result.extension = method.value;
}
return result;
}
/**
* @brief Convert wire format HttpMethod to user-facing type
*/
inline HttpMethod from_wire(const wire::HttpMethod& method) {
switch (method.tag) {
case wire::HttpMethod::Tag::Get:
return HttpMethod::get();
case wire::HttpMethod::Tag::Head:
return HttpMethod::head();
case wire::HttpMethod::Tag::Post:
return HttpMethod::post();
case wire::HttpMethod::Tag::Put:
return HttpMethod::put();
case wire::HttpMethod::Tag::Delete:
return HttpMethod::del();
case wire::HttpMethod::Tag::Connect:
return HttpMethod::connect();
case wire::HttpMethod::Tag::Options:
return HttpMethod::options();
case wire::HttpMethod::Tag::Trace:
return HttpMethod::trace();
case wire::HttpMethod::Tag::Patch:
return HttpMethod::patch();
case wire::HttpMethod::Tag::Extension:
return HttpMethod{method.extension};
default:
// Should never happen, but default to GET for safety
return HttpMethod::get();
}
}
// ==================== HttpVersion Conversions ====================
/**
* @brief Convert user-facing HttpVersion to wire format
*/
inline wire::HttpVersion to_wire(HttpVersion version) {
wire::HttpVersion result;
switch (version) {
case HttpVersion::Http09:
result.tag = wire::HttpVersion::Tag::Http09;
break;
case HttpVersion::Http10:
result.tag = wire::HttpVersion::Tag::Http10;
break;
case HttpVersion::Http11:
result.tag = wire::HttpVersion::Tag::Http11;
break;
case HttpVersion::Http2:
result.tag = wire::HttpVersion::Tag::Http2;
break;
case HttpVersion::Http3:
result.tag = wire::HttpVersion::Tag::Http3;
break;
}
return result;
}
/**
* @brief Convert wire format HttpVersion to user-facing type
*/
inline HttpVersion from_wire(const wire::HttpVersion& version) {
switch (version.tag) {
case wire::HttpVersion::Tag::Http09:
return HttpVersion::Http09;
case wire::HttpVersion::Tag::Http10:
return HttpVersion::Http10;
case wire::HttpVersion::Tag::Http11:
return HttpVersion::Http11;
case wire::HttpVersion::Tag::Http2:
return HttpVersion::Http2;
case wire::HttpVersion::Tag::Http3:
return HttpVersion::Http3;
default:
// Should never happen, default to HTTP/1.1
return HttpVersion::Http11;
}
}
// ==================== HttpHeader Conversions ====================
/**
* @brief Convert user-facing HttpHeader to wire format HttpHeaderPair
*
* WARNING: The is_sensitive flag is LOST during this conversion.
* The wire format does not preserve sensitivity information.
*/
inline wire::HttpHeaderPair to_wire(const HttpHeader& header) {
wire::HttpHeaderPair result;
result.name = header.name;
result.value = header.value;
return result;
}
/**
* @brief Convert wire format HttpHeaderPair to user-facing HttpHeader
*
* The resulting header will have is_sensitive=false, as the wire format
* does not preserve sensitivity information.
*/
inline HttpHeader from_wire(const wire::HttpHeaderPair& pair) {
return HttpHeader{pair.name, pair.value, false};
}
// ==================== HttpHeaders (collection) Conversions ====================
/**
* @brief Convert user-facing header vector to wire format HttpHeaders
*/
inline wire::HttpHeaders to_wire_headers(const std::vector<HttpHeader>& headers) {
wire::HttpHeaders result;
result.entries.reserve(headers.size());
for (const auto& header : headers) {
result.entries.push_back(to_wire(header));
}
return result;
}
/**
* @brief Convert wire format HttpHeaders to user-facing header vector
*/
inline std::vector<HttpHeader> from_wire_headers(const wire::HttpHeaders& headers) {
std::vector<HttpHeader> result;
result.reserve(headers.entries.size());
for (const auto& pair : headers.entries) {
result.push_back(from_wire(pair));
}
return result;
}
// ==================== HttpRequest Conversions ====================
/**
* @brief Convert user-facing HttpRequest to wire format
*
* Note: The body field is NOT included in the wire HttpRequest struct.
* The body bytes are passed separately via ConsumeBytes().
*/
inline wire::HttpRequest to_wire(const HttpRequest& request) {
wire::HttpRequest result;
result.method = to_wire(request.method);
result.headers = to_wire_headers(request.headers);
result.timeout = request.timeout;
result.uri = request.uri;
result.version = to_wire(request.version);
return result;
}
/**
* @brief Convert wire format HttpRequest to user-facing type
*
* Note: The returned HttpRequest will have an empty body.
* The body bytes are received separately via ConsumeBytes() and must be set manually.
*/
inline HttpRequest from_wire(const wire::HttpRequest& request) {
HttpRequest result;
result.method = from_wire(request.method);
result.headers = from_wire_headers(request.headers);
result.timeout = request.timeout;
result.uri = request.uri;
result.version = from_wire(request.version);
result.body = HttpBody::empty(); // Body is received separately
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 ====================
/**
* @brief Convert user-facing HttpResponse to wire format
*
* Note: The body field is NOT included in the wire HttpResponse struct.
* The body bytes are passed separately via ConsumeBytes().
*/
inline wire::HttpResponse to_wire(const HttpResponse& response) {
wire::HttpResponse result;
result.headers = to_wire_headers(response.headers);
result.version = to_wire(response.version);
result.code = response.status_code;
return result;
}
/**
* @brief Convert wire format HttpResponse to user-facing type
*
* Note: The returned HttpResponse will have an empty body.
* The body bytes are received separately via ConsumeBytes() and must be set manually.
*/
inline HttpResponse from_wire(const wire::HttpResponse& response) {
HttpResponse result;
result.headers = from_wire_headers(response.headers);
result.version = from_wire(response.version);
result.status_code = response.code;
result.body = HttpBody::empty(); // Body is received separately
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
#include "spacetimedb/logger.h"
#include "spacetimedb/http_client_impl.h"
#endif // SPACETIMEDB_HTTP_CONVERT_H