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)