diff --git a/crates/bindings-csharp/BSATN.Runtime/HttpWireTypes.cs b/crates/bindings-csharp/BSATN.Runtime/HttpWireTypes.cs
new file mode 100644
index 00000000000..b0efb0c3d4a
--- /dev/null
+++ b/crates/bindings-csharp/BSATN.Runtime/HttpWireTypes.cs
@@ -0,0 +1,77 @@
+namespace SpacetimeDB;
+
+using System.ComponentModel;
+
+// NOTE: These types define the stable BSATN wire format for the procedure_http_request ABI.
+// They must match `spacetimedb_lib::http::{Request, Response}` exactly (field order + types),
+// because the host BSATN-decodes these bytes directly and may trap on mismatch.
+// Do not reorder fields or extend these types; add a new versioned ABI instead.
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public partial record HttpMethodWire
+ : TaggedEnum<(
+ Unit Get,
+ Unit Head,
+ Unit Post,
+ Unit Put,
+ Unit Delete,
+ Unit Connect,
+ Unit Options,
+ Unit Trace,
+ Unit Patch,
+ string Extension
+ )>;
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public enum HttpVersionWire : byte
+{
+ Http09,
+ Http10,
+ Http11,
+ Http2,
+ Http3,
+}
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public partial struct HttpHeaderPairWire
+{
+ public string Name;
+ public byte[] Value;
+}
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public partial struct HttpHeadersWire
+{
+ public HttpHeaderPairWire[] Entries;
+}
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public partial struct HttpTimeoutWire
+{
+ public TimeDuration Timeout;
+}
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public partial struct HttpRequestWire
+{
+ public HttpMethodWire Method;
+ public HttpHeadersWire Headers;
+ public HttpTimeoutWire? Timeout;
+ public string Uri;
+ public HttpVersionWire Version;
+}
+
+[Type]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public partial struct HttpResponseWire
+{
+ public HttpHeadersWire Headers;
+ public HttpVersionWire Version;
+ public ushort Code;
+}
diff --git a/crates/bindings-csharp/Runtime/Exceptions.cs b/crates/bindings-csharp/Runtime/Exceptions.cs
index a7a3cf8b09d..5255c8c64f7 100644
--- a/crates/bindings-csharp/Runtime/Exceptions.cs
+++ b/crates/bindings-csharp/Runtime/Exceptions.cs
@@ -98,6 +98,11 @@ public class TransactionIsMutableException : StdbException
"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;
diff --git a/crates/bindings-csharp/Runtime/Http.cs b/crates/bindings-csharp/Runtime/Http.cs
new file mode 100644
index 00000000000..6d23dc72ef4
--- /dev/null
+++ b/crates/bindings-csharp/Runtime/Http.cs
@@ -0,0 +1,443 @@
+namespace SpacetimeDB;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Internal;
+using SpacetimeDB.BSATN;
+
+public enum HttpVersion : byte
+{
+ Http09,
+ Http10,
+ Http11,
+ Http2,
+ Http3,
+}
+
+///
+/// Represents an HTTP method (e.g. GET, POST).
+///
+///
+/// Unknown methods are supported by providing an arbitrary string value.
+///
+public readonly record struct HttpMethod(string Value)
+{
+ public static readonly HttpMethod Get = new("GET");
+ public static readonly HttpMethod Head = new("HEAD");
+ public static readonly HttpMethod Post = new("POST");
+ public static readonly HttpMethod Put = new("PUT");
+ public static readonly HttpMethod Delete = new("DELETE");
+ public static readonly HttpMethod Connect = new("CONNECT");
+ public static readonly HttpMethod Options = new("OPTIONS");
+ public static readonly HttpMethod Trace = new("TRACE");
+ public static readonly HttpMethod Patch = new("PATCH");
+}
+
+///
+/// Represents an HTTP header name/value pair.
+///
+///
+/// Multiple headers with the same name are permitted.
+/// The IsSensitive flag is a local-only hint and is not transmitted to the host.
+///
+public readonly record struct HttpHeader(string Name, byte[] Value, bool IsSensitive = false)
+{
+ public HttpHeader(string name, string value)
+ : this(name, Encoding.ASCII.GetBytes(value), false) { }
+}
+
+///
+/// Represents the body of an HTTP request or response.
+///
+///
+/// Bodies are treated as raw bytes. Use when interpreting a body as UTF-8 text.
+///
+public readonly record struct HttpBody(byte[] Bytes)
+{
+ public static HttpBody Empty => new(Array.Empty());
+
+ public byte[] ToBytes() => Bytes;
+
+ public string ToStringUtf8Lossy() => Encoding.UTF8.GetString(Bytes);
+
+ public static HttpBody FromString(string s) => new(Encoding.UTF8.GetBytes(s));
+}
+
+///
+/// Represents an HTTP request to be executed by the SpacetimeDB host from within a procedure.
+///
+///
+/// The request body is stored separately from the request metadata in the host ABI.
+///
+public sealed class HttpRequest
+{
+ /// Request URI.
+ /// Must not be null or empty.
+ public required string Uri { get; init; }
+
+ /// HTTP method to use (e.g. GET, POST).
+ public HttpMethod Method { get; init; } = HttpMethod.Get;
+
+ /// HTTP headers to include with the request.
+ public List Headers { get; init; } = new();
+
+ /// Request body bytes.
+ public HttpBody Body { get; init; } = HttpBody.Empty;
+
+ /// HTTP version to report in the request metadata.
+ public HttpVersion Version { get; init; } = HttpVersion.Http11;
+
+ ///
+ /// Optional timeout for the request.
+ ///
+ ///
+ /// The SpacetimeDB host clamps all timeouts to a maximum of 500ms.
+ ///
+ public TimeSpan? Timeout { get; init; }
+}
+
+///
+/// Represents an HTTP response returned by the SpacetimeDB host.
+///
+///
+/// A non-2xx status code is still returned as a successful response; callers should inspect
+/// to handle application-level errors from the remote server.
+///
+public readonly record struct HttpResponse(
+ ushort StatusCode,
+ HttpVersion Version,
+ List Headers,
+ HttpBody Body
+);
+
+///
+/// Error returned when the SpacetimeDB host could not execute an HTTP request.
+///
+///
+/// This indicates a failure to perform the request (e.g. DNS failure, connection error, timeout),
+/// not an application-level HTTP error response (which is represented by ).
+///
+public sealed class HttpError(string message) : Exception(message)
+{
+ private readonly string message = message;
+
+ public override string Message => message;
+}
+
+///
+/// Allows a procedure to perform outbound HTTP requests via the host.
+///
+///
+/// This API is available from ProcedureContext.Http.
+///
+/// The request metadata (method/headers/timeout/uri/version) is encoded using a stable wire format
+/// and executed by the SpacetimeDB host. The request body is sent separately as raw bytes.
+///
+///
+/// Transaction limitation: HTTP requests cannot be performed while a mutable transaction is open.
+/// If called inside WithTx, the host will reject the call (WOULD_BLOCK_TRANSACTION).
+///
+///
+///
+/// Timeouts: The host clamps all HTTP timeouts to a maximum of 500ms.
+///
+///
+///
+/// The returned response may have any HTTP status code (including non-2xx). This is still considered a
+/// successful HTTP exchange; only returns an error when the request could not be
+/// initiated or completed (e.g. DNS failure, connection failure, timeout).
+///
+///
+public sealed class HttpClient
+{
+ private static readonly TimeSpan MaxTimeout = TimeSpan.FromMilliseconds(500);
+
+ ///
+ /// Send a simple GET request to with no headers.
+ ///
+ /// The request URI.
+ ///
+ /// Optional timeout for the request. The host clamps timeouts to a maximum of 500ms.
+ ///
+ ///
+ /// Ok(HttpResponse) when a response was received (regardless of HTTP status code),
+ /// or Err(HttpError) if the request failed to execute.
+ ///
+ ///
+ ///
+ /// [SpacetimeDB.Procedure]
+ /// public static string FetchSchema(ProcedureContext ctx)
+ /// {
+ /// var result = ctx.Http.Get("http://localhost:3000/v1/database/schema");
+ /// if (!result.IsSuccess)
+ /// {
+ /// return $"ERR {result.Error}";
+ /// }
+ ///
+ /// var response = result.Value!;
+ /// return response.Body.ToStringUtf8Lossy();
+ /// }
+ ///
+ ///
+ public Result Get(string uri, TimeSpan? timeout = null) =>
+ Send(
+ new HttpRequest
+ {
+ Uri = uri,
+ Method = HttpMethod.Get,
+ Body = HttpBody.Empty,
+ Timeout = timeout,
+ }
+ );
+
+ ///
+ /// Send an HTTP request described by and wait for its response.
+ ///
+ ///
+ /// Request metadata (method, headers, uri, version, optional timeout) plus a request body.
+ ///
+ ///
+ /// Ok(HttpResponse) when a response was received (including non-2xx status codes),
+ /// or Err(HttpError) when the host could not perform the request.
+ ///
+ ///
+ /// This method does not throw for expected failures; errors are returned as Result.Err.
+ ///
+ ///
+ ///
+ /// [SpacetimeDB.Procedure]
+ /// public static string PostSomething(ProcedureContext ctx)
+ /// {
+ /// var request = new HttpRequest
+ /// {
+ /// Uri = "https://some-remote-host.invalid/upload",
+ /// Method = new HttpMethod("POST"),
+ /// Headers = new()
+ /// {
+ /// new HttpHeader("Content-Type", "text/plain"),
+ /// },
+ /// Body = HttpBody.FromString("This is the body of the HTTP request"),
+ /// Timeout = TimeSpan.FromMilliseconds(100),
+ /// };
+ ///
+ /// var result = ctx.Http.Send(request);
+ /// if (!result.IsSuccess)
+ /// {
+ /// return $"ERR {result.Error}";
+ /// }
+ ///
+ /// var response = result.Value!;
+ /// return $"OK status={response.StatusCode} body={response.Body.ToStringUtf8Lossy()}";
+ /// }
+ ///
+ ///
+ ///
+ ///
+ /// [SpacetimeDB.Procedure]
+ /// public static string FetchMay404(ProcedureContext ctx)
+ /// {
+ /// var result = ctx.Http.Get("https://example.invalid/missing");
+ /// if (!result.IsSuccess)
+ /// {
+ /// // DNS failure, connection drop, timeout, etc.
+ /// return $"ERR transport: {result.Error}";
+ /// }
+ ///
+ /// var response = result.Value!;
+ /// if (response.StatusCode != 200)
+ /// {
+ /// // Application-level HTTP error response.
+ /// return $"ERR http status={response.StatusCode}";
+ /// }
+ ///
+ /// return $"OK {response.Body.ToStringUtf8Lossy()}";
+ /// }
+ ///
+ ///
+ ///
+ ///
+ /// [SpacetimeDB.Procedure]
+ /// public static void DontDoThis(ProcedureContext ctx)
+ /// {
+ /// ctx.WithTx(tx =>
+ /// {
+ /// // The host rejects this with WOULD_BLOCK_TRANSACTION.
+ /// var _ = ctx.Http.Get("https://example.invalid/");
+ /// return 0;
+ /// });
+ /// }
+ ///
+ ///
+ public Result Send(HttpRequest request)
+ {
+ // The host syscall expects BSATN-encoded spacetimedb_lib::http::Request bytes.
+ // A mismatch in the wire layout can cause the host to trap during BSATN decode,
+ // so the C# `Http*Wire` types must remain in lockstep with the Rust definitions.
+ try
+ {
+ if (string.IsNullOrEmpty(request.Uri))
+ {
+ return Result.Err(
+ new HttpError("URI must not be null or empty")
+ );
+ }
+
+ // The host clamps all HTTP timeouts to a maximum of 500ms.
+ // Clamp here as well to keep C# behavior aligned with the Rust docs and to reduce surprises.
+ var timeout = request.Timeout;
+ if (timeout is not null)
+ {
+ if (timeout.Value < TimeSpan.Zero)
+ {
+ return Result.Err(
+ new HttpError("Timeout must not be negative")
+ );
+ }
+
+ if (timeout.Value > MaxTimeout)
+ {
+ timeout = MaxTimeout;
+ }
+ }
+
+ var requestWire = new HttpRequestWire
+ {
+ Method = ToWireMethod(request.Method),
+ Headers = new HttpHeadersWire
+ {
+ Entries = request.Headers.Select(ToWireHeader).ToArray(),
+ },
+ Timeout = timeout is null
+ ? null
+ : new HttpTimeoutWire { Timeout = (TimeDuration)timeout.Value },
+ Uri = request.Uri,
+ Version = ToWireVersion(request.Version),
+ };
+
+ var requestBytes = IStructuralReadWrite.ToBytes(
+ new HttpRequestWire.BSATN(),
+ requestWire
+ );
+ var bodyBytes = request.Body.ToBytes();
+
+ var status = FFI.procedure_http_request(
+ requestBytes,
+ (uint)requestBytes.Length,
+ bodyBytes,
+ (uint)bodyBytes.Length,
+ out var out_
+ );
+
+ switch (status)
+ {
+ case Errno.OK:
+ {
+ var responseWireBytes = out_.A.Consume();
+ var responseWire = FromBytes(new HttpResponseWire.BSATN(), responseWireBytes);
+
+ var body = new HttpBody(out_.B.Consume());
+ var (statusCode, version, headers) = FromWireResponse(responseWire);
+
+ return Result.Ok(
+ new HttpResponse(statusCode, version, headers, body)
+ );
+ }
+ case Errno.HTTP_ERROR:
+ {
+ var errorWireBytes = out_.A.Consume();
+ var err = FromBytes(new SpacetimeDB.BSATN.String(), errorWireBytes);
+ return Result.Err(new HttpError(err));
+ }
+ case Errno.WOULD_BLOCK_TRANSACTION:
+ return Result.Err(
+ new HttpError(
+ "HTTP requests cannot be performed while a mutable transaction is open (WOULD_BLOCK_TRANSACTION)."
+ )
+ );
+ default:
+ return Result.Err(
+ new HttpError(FFI.ErrnoHelpers.ToException(status).ToString())
+ );
+ }
+ }
+ // Important: avoid throwing across the procedure boundary.
+ // Throwing here would trap the module (and fail the whole procedure invocation).
+ // Convert all unexpected failures (including decode errors / unexpected errno) into Result.Err instead.
+ catch (Exception ex)
+ {
+ return Result.Err(new HttpError(ex.ToString()));
+ }
+ }
+
+ private static T FromBytes(IReadWrite rw, byte[] bytes)
+ {
+ using var ms = new MemoryStream(bytes);
+ using var reader = new BinaryReader(ms);
+ var value = rw.Read(reader);
+ if (ms.Position != ms.Length)
+ {
+ throw new InvalidOperationException(
+ "Unrecognized extra bytes while decoding BSATN value"
+ );
+ }
+ return value;
+ }
+
+ private static HttpMethodWire ToWireMethod(HttpMethod method)
+ {
+ var m = method.Value;
+ return m switch
+ {
+ "GET" => new HttpMethodWire.Get(default),
+ "HEAD" => new HttpMethodWire.Head(default),
+ "POST" => new HttpMethodWire.Post(default),
+ "PUT" => new HttpMethodWire.Put(default),
+ "DELETE" => new HttpMethodWire.Delete(default),
+ "CONNECT" => new HttpMethodWire.Connect(default),
+ "OPTIONS" => new HttpMethodWire.Options(default),
+ "TRACE" => new HttpMethodWire.Trace(default),
+ "PATCH" => new HttpMethodWire.Patch(default),
+ _ => new HttpMethodWire.Extension(m),
+ };
+ }
+
+ private static HttpVersionWire ToWireVersion(HttpVersion version) =>
+ version switch
+ {
+ HttpVersion.Http09 => HttpVersionWire.Http09,
+ HttpVersion.Http10 => HttpVersionWire.Http10,
+ HttpVersion.Http11 => HttpVersionWire.Http11,
+ HttpVersion.Http2 => HttpVersionWire.Http2,
+ HttpVersion.Http3 => HttpVersionWire.Http3,
+ _ => throw new ArgumentOutOfRangeException(nameof(version)),
+ };
+
+ private static HttpHeaderPairWire ToWireHeader(HttpHeader header) =>
+ new() { Name = header.Name, Value = header.Value };
+
+ private static (
+ ushort statusCode,
+ HttpVersion version,
+ List headers
+ ) FromWireResponse(HttpResponseWire responseWire)
+ {
+ var version = responseWire.Version switch
+ {
+ HttpVersionWire.Http09 => HttpVersion.Http09,
+ HttpVersionWire.Http10 => HttpVersion.Http10,
+ HttpVersionWire.Http11 => HttpVersion.Http11,
+ HttpVersionWire.Http2 => HttpVersion.Http2,
+ HttpVersionWire.Http3 => HttpVersion.Http3,
+ _ => throw new InvalidOperationException("Invalid HTTP version returned from host"),
+ };
+
+ var headers = responseWire
+ .Headers.Entries.Select(h => new HttpHeader(h.Name, h.Value, false))
+ .ToList();
+
+ return (responseWire.Code, version, headers);
+ }
+}
diff --git a/crates/bindings-csharp/Runtime/Internal/FFI.cs b/crates/bindings-csharp/Runtime/Internal/FFI.cs
index dec9c8f25df..9cf994e929b 100644
--- a/crates/bindings-csharp/Runtime/Internal/FFI.cs
+++ b/crates/bindings-csharp/Runtime/Internal/FFI.cs
@@ -41,6 +41,7 @@ public enum Errno : short
TRANSACTION_NOT_ANONYMOUS = 18,
TRANSACTION_IS_READ_ONLY = 19,
TRANSACTION_IS_MUT = 20,
+ HTTP_ERROR = 21,
}
#pragma warning disable IDE1006 // Naming Styles - Not applicable to FFI stuff.
@@ -144,6 +145,7 @@ public static Exception ToException(Errno status) =>
Errno.TRANSACTION_NOT_ANONYMOUS => new TransactionNotAnonymousException(),
Errno.TRANSACTION_IS_READ_ONLY => new TransactionIsReadOnlyException(),
Errno.TRANSACTION_IS_MUT => new TransactionIsMutableException(),
+ Errno.HTTP_ERROR => new HttpException(),
_ => new UnknownException(status),
};
}
@@ -379,4 +381,20 @@ uint args_len
[LibraryImport(StdbNamespace10_3, EntryPoint = "procedure_abort_mut_tx")]
public static partial Errno procedure_abort_mut_tx();
+
+ [StructLayout(LayoutKind.Sequential)]
+ public readonly struct BytesSourcePair
+ {
+ public readonly BytesSource A;
+ public readonly BytesSource B;
+ }
+
+ [LibraryImport(StdbNamespace10_3, EntryPoint = "procedure_http_request")]
+ public static partial Errno procedure_http_request(
+ ReadOnlySpan request,
+ uint request_len,
+ ReadOnlySpan body,
+ uint body_len,
+ out BytesSourcePair out_
+ );
}
diff --git a/crates/bindings-csharp/Runtime/Internal/Module.cs b/crates/bindings-csharp/Runtime/Internal/Module.cs
index a74b981f37c..c9f528258d8 100644
--- a/crates/bindings-csharp/Runtime/Internal/Module.cs
+++ b/crates/bindings-csharp/Runtime/Internal/Module.cs
@@ -347,17 +347,7 @@ BytesSink resultSink
using var stream = new MemoryStream(args.Consume());
using var reader = new BinaryReader(stream);
- var bytes = Array.Empty();
- try
- {
- bytes = procedures[(int)id].Invoke(reader, ctx);
- }
- catch (Exception e)
- {
- var errorBytes = System.Text.Encoding.UTF8.GetBytes(e.ToString());
- resultSink.Write(errorBytes);
- return Errno.HOST_CALL_FAILURE;
- }
+ var bytes = procedures[(int)id].Invoke(reader, ctx);
if (stream.Position != stream.Length)
{
throw new Exception("Unrecognised extra bytes in the procedure arguments");
@@ -368,9 +358,11 @@ BytesSink resultSink
}
catch (Exception e)
{
- var errorBytes = System.Text.Encoding.UTF8.GetBytes(e.ToString());
- resultSink.Write(errorBytes);
- return Errno.HOST_CALL_FAILURE;
+ // Host contract __call_procedure__ must either return Errno.OK or trap.
+ // Returning other errno values here can put the host/runtime in an unexpected state,
+ // so we log and rethrow to trap on any exception.
+ Log.Error($"Error while invoking procedure: {e}");
+ throw;
}
}
diff --git a/crates/bindings-csharp/Runtime/ProcedureContext.cs b/crates/bindings-csharp/Runtime/ProcedureContext.cs
index e11903e327e..cf20388128c 100644
--- a/crates/bindings-csharp/Runtime/ProcedureContext.cs
+++ b/crates/bindings-csharp/Runtime/ProcedureContext.cs
@@ -1,7 +1,6 @@
namespace SpacetimeDB;
using System.Diagnostics.CodeAnalysis;
-using Internal;
public readonly struct Result(bool isSuccess, T? value, E? error)
where E : Exception
@@ -52,6 +51,10 @@ Timestamp time
public Timestamp Timestamp { get; private set; } = time;
public AuthCtx SenderAuth { get; } = AuthCtx.BuildFromSystemTables(connectionId, sender);
+ // NOTE: The host rejects procedure HTTP requests while a mut transaction is open
+ // (WOULD_BLOCK_TRANSACTION). Avoid calling `Http.*` inside WithTx.
+ public HttpClient Http { get; } = new();
+
// **Note:** must be 0..=u32::MAX
protected int CounterUuid = 0;
private Internal.TxContext? txContext;
diff --git a/crates/bindings-csharp/Runtime/bindings.c b/crates/bindings-csharp/Runtime/bindings.c
index 33fa039fa4d..1b55d095713 100644
--- a/crates/bindings-csharp/Runtime/bindings.c
+++ b/crates/bindings-csharp/Runtime/bindings.c
@@ -122,6 +122,11 @@ IMPORT(int16_t, get_jwt, (const uint8_t* connection_id_ptr, BytesSource* bytes_p
IMPORT(uint16_t, procedure_start_mut_tx, (int64_t* micros), (micros));
IMPORT(uint16_t, procedure_commit_mut_tx, (void), ());
IMPORT(uint16_t, procedure_abort_mut_tx, (void), ());
+IMPORT(uint16_t, procedure_http_request,
+ (const uint8_t* request_ptr, uint32_t request_len,
+ const uint8_t* body_ptr, uint32_t body_len,
+ BytesSource* out),
+ (request_ptr, request_len, body_ptr, body_len, out));
#undef SPACETIME_MODULE_VERSION
#ifndef EXPERIMENTAL_WASM_AOT
diff --git a/sdks/csharp/examples~/regression-tests/client/Program.cs b/sdks/csharp/examples~/regression-tests/client/Program.cs
index 16d927fcff1..dbaf10390b3 100644
--- a/sdks/csharp/examples~/regression-tests/client/Program.cs
+++ b/sdks/csharp/examples~/regression-tests/client/Program.cs
@@ -1,4 +1,4 @@
-/// Regression tests run with a live server.
+/// Regression tests run with a live server.
/// To run these, run a local SpacetimeDB via `spacetime start`,
/// then in a separate terminal run `tools~/run-regression-tests.sh PATH_TO_SPACETIMEDB_REPO_CHECKOUT`.
/// This is done on CI in .github/workflows/test.yml.
@@ -231,6 +231,42 @@ void OnSubscriptionApplied(SubscriptionEventContext context)
Debug.Assert(anonViewRemoteQueryRows.Result.First().Equals(expectedPlayerAndLevel));
// Procedures tests
+ Log.Debug("Calling ReadMySchemaViaHttp");
+ waiting++;
+ context.Procedures.ReadMySchemaViaHttp((IProcedureEventContext ctx, ProcedureCallbackResult result) =>
+ {
+ try
+ {
+ Debug.Assert(result.IsSuccess, $"ReadMySchemaViaHttp should succeed. Error received: {result.Error}");
+ Debug.Assert(result.Value != null, "ReadMySchemaViaHttp should return a string");
+ Debug.Assert(result.Value.StartsWith("OK "), $"Expected OK prefix, got: {result.Value}");
+ Debug.Assert(
+ result.Value.Contains("example_data"),
+ $"Expected schema response to mention example_data, got: {result.Value}"
+ );
+ }
+ finally
+ {
+ waiting--;
+ }
+ });
+
+ Log.Debug("Calling InvalidHttpRequest");
+ waiting++;
+ context.Procedures.InvalidHttpRequest((IProcedureEventContext ctx, ProcedureCallbackResult result) =>
+ {
+ try
+ {
+ Debug.Assert(result.IsSuccess, $"InvalidHttpRequest should succeed. Error received: {result.Error}");
+ Debug.Assert(result.Value != null, "InvalidHttpRequest should return a string");
+ Debug.Assert(result.Value.StartsWith("ERR "), $"Expected ERR prefix, got: {result.Value}");
+ }
+ finally
+ {
+ waiting--;
+ }
+ });
+
Log.Debug("Calling InsertWithTxRollback");
waiting++;
context.Procedures.InsertWithTxRollback((IProcedureEventContext ctx, ProcedureCallbackResult result) =>
diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/InvalidHttpRequest.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/InvalidHttpRequest.g.cs
new file mode 100644
index 00000000000..7e76023b9b8
--- /dev/null
+++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/InvalidHttpRequest.g.cs
@@ -0,0 +1,65 @@
+// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
+// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
+
+#nullable enable
+
+using System;
+using SpacetimeDB.ClientApi;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace SpacetimeDB.Types
+{
+ public sealed partial class RemoteProcedures : RemoteBase
+ {
+ public void InvalidHttpRequest(ProcedureCallback callback)
+ {
+ // Convert the clean callback to the wrapper callback
+ InternalInvalidHttpRequest((ctx, result) =>
+ {
+ if (result.IsSuccess && result.Value != null)
+ {
+ callback(ctx, ProcedureCallbackResult.Success(result.Value.Value));
+ }
+ else
+ {
+ callback(ctx, ProcedureCallbackResult.Failure(result.Error!));
+ }
+ });
+ }
+
+ private void InternalInvalidHttpRequest(ProcedureCallback callback)
+ {
+ conn.InternalCallProcedure(new Procedure.InvalidHttpRequestArgs(), callback);
+ }
+
+ }
+
+ public abstract partial class Procedure
+ {
+ [SpacetimeDB.Type]
+ [DataContract]
+ public sealed partial class InvalidHttpRequest
+ {
+ [DataMember(Name = "Value")]
+ public string Value;
+
+ public InvalidHttpRequest(string Value)
+ {
+ this.Value = Value;
+ }
+
+ public InvalidHttpRequest()
+ {
+ this.Value = "";
+ }
+ }
+ [SpacetimeDB.Type]
+ [DataContract]
+ public sealed partial class InvalidHttpRequestArgs : Procedure, IProcedureArgs
+ {
+ string IProcedureArgs.ProcedureName => "InvalidHttpRequest";
+ }
+
+ }
+}
diff --git a/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs b/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs
new file mode 100644
index 00000000000..11e2a8b883f
--- /dev/null
+++ b/sdks/csharp/examples~/regression-tests/client/module_bindings/Procedures/ReadMySchemaViaHttp.g.cs
@@ -0,0 +1,65 @@
+// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
+// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
+
+#nullable enable
+
+using System;
+using SpacetimeDB.ClientApi;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace SpacetimeDB.Types
+{
+ public sealed partial class RemoteProcedures : RemoteBase
+ {
+ public void ReadMySchemaViaHttp(ProcedureCallback callback)
+ {
+ // Convert the clean callback to the wrapper callback
+ InternalReadMySchemaViaHttp((ctx, result) =>
+ {
+ if (result.IsSuccess && result.Value != null)
+ {
+ callback(ctx, ProcedureCallbackResult.Success(result.Value.Value));
+ }
+ else
+ {
+ callback(ctx, ProcedureCallbackResult.Failure(result.Error!));
+ }
+ });
+ }
+
+ private void InternalReadMySchemaViaHttp(ProcedureCallback callback)
+ {
+ conn.InternalCallProcedure(new Procedure.ReadMySchemaViaHttpArgs(), callback);
+ }
+
+ }
+
+ public abstract partial class Procedure
+ {
+ [SpacetimeDB.Type]
+ [DataContract]
+ public sealed partial class ReadMySchemaViaHttp
+ {
+ [DataMember(Name = "Value")]
+ public string Value;
+
+ public ReadMySchemaViaHttp(string Value)
+ {
+ this.Value = Value;
+ }
+
+ public ReadMySchemaViaHttp()
+ {
+ this.Value = "";
+ }
+ }
+ [SpacetimeDB.Type]
+ [DataContract]
+ public sealed partial class ReadMySchemaViaHttpArgs : Procedure, IProcedureArgs
+ {
+ string IProcedureArgs.ProcedureName => "ReadMySchemaViaHttp";
+ }
+
+ }
+}
diff --git a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs
index adb0e51c5b8..5e9e56dff57 100644
--- a/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs
+++ b/sdks/csharp/examples~/regression-tests/republishing/client/module_bindings/SpacetimeDBClient.g.cs
@@ -1,7 +1,7 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
-// This was generated using spacetimedb cli version 1.9.0 (commit a722cabfdf0a59d6fd152cf316511a514475c0ca).
+// This was generated using spacetimedb cli version 1.11.1 (commit 41eec04ea6150114247ff4ae7cbd7a68b1144bd5).
#nullable enable
diff --git a/sdks/csharp/examples~/regression-tests/server/Lib.cs b/sdks/csharp/examples~/regression-tests/server/Lib.cs
index 8a67fe947b4..f6e2477457c 100644
--- a/sdks/csharp/examples~/regression-tests/server/Lib.cs
+++ b/sdks/csharp/examples~/regression-tests/server/Lib.cs
@@ -2,6 +2,7 @@
// Everything we're testing for happens SDK-side so this module is very uninteresting.
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using SpacetimeDB;
[SpacetimeDB.Type]
@@ -213,6 +214,42 @@ public static SpacetimeDB.Unit WillPanic(ProcedureContext ctx)
throw new InvalidOperationException("This procedure is expected to panic");
}
+ [SpacetimeDB.Procedure]
+ [Experimental("STDB_UNSTABLE")]
+ public static string ReadMySchemaViaHttp(ProcedureContext ctx)
+ {
+ try
+ {
+ var moduleIdentity = ProcedureContext.Identity;
+ var uri = $"http://localhost:3000/v1/database/{moduleIdentity}/schema?version=9";
+ var res = ctx.Http.Get(uri, System.TimeSpan.FromSeconds(2));
+ return res.IsSuccess
+ ? "OK " + res.Value!.Body.ToStringUtf8Lossy()
+ : "ERR " + res.Error!.Message;
+ }
+ catch (Exception e)
+ {
+ return "EXN " + e;
+ }
+ }
+
+ [SpacetimeDB.Procedure]
+ [Experimental("STDB_UNSTABLE")]
+ public static string InvalidHttpRequest(ProcedureContext ctx)
+ {
+ try
+ {
+ var res = ctx.Http.Get("http://foo.invalid/", System.TimeSpan.FromMilliseconds(250));
+ return res.IsSuccess
+ ? "OK " + res.Value!.Body.ToStringUtf8Lossy()
+ : "ERR " + res.Error!.Message;
+ }
+ catch (Exception e)
+ {
+ return "EXN " + e;
+ }
+ }
+
#pragma warning disable STDB_UNSTABLE
[SpacetimeDB.Procedure]
public static void InsertWithTxCommit(ProcedureContext ctx)