Files
Ryan 8dd18f078f C# bindings: add procedure_http_request support + fix HTTP BSATN wire format to match spacetimedb_lib::http (#3944)
# Description of Changes

Rust added procedure-scoped HTTP via the `procedure_http_request` ABI
(see Rust PR #3684) to C# host bindings.

What changed:
1) Fix for BSATN wire format
* Updated C# BSATN wire types for HTTP
(`BSATN.Runtime/HttpWireTypes.cs`) to match exactly the Rust stable
types:
* `spacetimedb_lib::http::Request` fields and order: `method`,
`headers`, `timeout`, `uri`, `version`
* `spacetimedb_lib::http::Response` fields and order: `headers`,
`version`, `code`
* Header pairs are (`name: string, value: bytes`); no additional
metadata is encoded.
* Added explicit “do not reorder/extend” note in the C# wire type file
since the host BSATN-decodes these bytes directly and may trap on
mismatch.
2) C# runtime HTTP client implementation
* Implemented `ProcedureContext.Http` support (`Runtime/Http.cs`) that:
* BSATN-serializes `HttpRequestWire` + sends body bytes via
`procedure_http_request`
* On success, BSATN-decodes `HttpResponseWire` and returns `(response,
body)` as a typed `HttpResponse`
  * On `HTTP_ERROR`, decodes the error payload as a BSATN `string`
* On `WOULD_BLOCK_TRANSACTION`, returns a stable error message (host
rejects blocking HTTP while a mut tx is open)
* Clamps timeouts to 500ms max (matching host behavior documented on the
Rust side)
3) ABI wiring / import plumbing
* Added the `procedure_http_request` import to the C# wasm shim
(`Runtime/bindings.c`) under the `spacetime_10.3` import module.
* Verified the generated C# P/Invoke signature matches the host ABI
(request ptr/len, body ptr/len, out `[BytesSource; 2]`).
4) Procedure entrypoint contract (trap vs errno)
* Updated/clarified the module procedure trampoline behavior
(`Runtime/Internal/Module.call_procedure`): it must either return
`Errno.OK` or trap (log + rethrow). This mirrors the host’s expectations
and avoids “unexpected errno values from guest entrypoints” behavior.
Behavioral notes vs Rust
* Rust bindings may `expect(...)`/panic on “should never happen”
serialization failures; C# runtime explicitly avoids throwing across the
procedure boundary for the HTTP client path and instead returns
`Result.Err` for unexpected failures. This prevents whole-module traps
from user-space HTTP usage while still surfacing rich errors.
* The host remains authoritative on timeout enforcement; C# now mirrors
the effective host clamp (500ms) to keep behavior aligned.

# API and ABI breaking changes

No new host ABI introduced, and does not change the syscall signature.
* This is not an ABI “break” in the host, but it is a guest-side wire
compatibility correction: older C# guests built with the incorrect wire
types could cause the host to trap during BSATN decode. After this PR,
C# guests are compatible with the host’s expected layout.

**Adds API**: `ProcedureContextBase.Http` is available for procedures
No existing public types/method signatures are removed
* A notable difference from Rust's panic behavior is that Rust code
sometimes `expect(...)`s and may panic/trap on internal “should not
happen” cases. On the other hand, C#'s `HttpClient.Send` explicitly
converts unexpected failures into `Result.Err` to avoid trapping the
module. This is a behavioral difference, but not an API break.

# Expected complexity level and risk

2 - Mostly just creating equivalents to the Rust version. 

# Testing

C# regression tests updated to cover:
- [X] Successful request path (`ReadMySchemaViaHttp`)
- [X] Invalid request path (`InvalidHttpRequest`) returning error
without trapping

Testing performed:
- [X] Full regression suite run against local node (see chat log); no
wasm traps, all suites reported `Success`.

---------

Signed-off-by: Ryan <r.ekhoff@clockworklabs.io>
2026-01-06 22:51:24 +00:00

114 lines
3.0 KiB
C#

namespace SpacetimeDB;
using SpacetimeDB.Internal;
public abstract class StdbException : Exception
{
public abstract override string Message { get; }
}
public class NotInTransactionException : StdbException
{
public override string Message => "ABI call can only be made while in a transaction";
}
public class BsatnDecodeException : StdbException
{
public override string Message => "Couldn't decode the BSATN to the expected type";
}
public class NoSuchTableException : StdbException
{
public override string Message => "No such table";
}
public class NoSuchIndexException : StdbException
{
public override string Message => "No such index";
}
public class IndexNotUniqueException : StdbException
{
public override string Message => "The index was not unique";
}
public class NoSuchRowException : StdbException
{
public override string Message => "The row was not found, e.g., in an update call";
}
public class UniqueConstraintViolationException : StdbException
{
public override string Message => "Value with given unique identifier already exists";
}
public class ScheduleAtDelayTooLongException : StdbException
{
public override string Message => "Specified delay in scheduling row was too long";
}
public class BufferTooSmallException : StdbException
{
public override string Message => "The provided buffer is not large enough to store the data";
}
public class NoSuchIterException : StdbException
{
public override string Message => "The provided row iterator does not exist";
}
public class NoSuchLogStopwatch : StdbException
{
public override string Message => "The provided stopwatch does not exist";
}
public class NoSuchBytesException : StdbException
{
public override string Message => "The provided bytes source or sink does not exist";
}
public class NoSpaceException : StdbException
{
public override string Message => "The provided bytes sink has no more room left";
}
public class AutoIncOverflowException : StdbException
{
public override string Message => "The auto-increment sequence overflowed";
}
public class TransactionWouldBlockException : StdbException
{
public override string Message => "Attempted operation while another transaction is open";
}
public class TransactionNotAnonymousException : StdbException
{
public override string Message => "The transaction is not anonymous";
}
public class TransactionIsReadOnlyException : StdbException
{
public override string Message => "The transaction is read-only";
}
public class TransactionIsMutableException : StdbException
{
public override string Message =>
"ABI call can only be made while inside a read-only transaction";
}
public class HttpException : StdbException
{
public override string Message => "HTTP request failed";
}
public class UnknownException : StdbException
{
private readonly Errno code;
internal UnknownException(Errno code) => this.code = code;
public override string Message => $"SpacetimeDB error code {code}";
}