mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-07-24 19:12:59 -04:00
d5c75d944d
# 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>
288 lines
9.2 KiB
C++
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
|