clockworklabs/SpacetimeDB · error · InvalidOperationException

Reducer arguments type {typeof(T).FullName} is not assignabl

Error message

Reducer arguments type {typeof(T).FullName} is not assignable to {typeof(Reducer).FullName}.

What it means

InternalCallReducer<T> accepts any T implementing IReducerArgs (which only requires ReducerName + BSATN read/write), but the pending-call machinery stores the args as the Reducer base class so the onReducer callback can re-deliver them. If the runtime type is not a Reducer instance, the method throws InvalidOperationException after the request id was already allocated. Generated reducer wrappers always satisfy this; hand-written args types may not.

Source

Thrown at sdks/csharp/src/SpacetimeDBClient.cs:875

        // Note: this method is called from unit tests.
        internal void OnMessageReceived(byte[] bytes, DateTime timestamp)
        {
            _parseQueue.Add(new UnparsedMessage { bytes = bytes, timestamp = timestamp, parseQueueTrackerId = stats.ParseMessageQueueTracker.StartTrackingRequest() });
        }

        void IDbConnection.InternalCallReducer<T>(T args)
        {
            if (!webSocket.IsConnected)
            {
                Log.Error("Cannot call reducer, not connected to server!");
                return;
            }

            var requestId = stats.ReducerRequestTracker.StartTrackingRequest(args.ReducerName);
            if (args is not Reducer typedReducer)
            {
                throw new InvalidOperationException(
                    $"Reducer arguments type {typeof(T).FullName} is not assignable to {typeof(Reducer).FullName}."
                );
            }

            var encodedArgs = IStructuralReadWrite.ToBytes(args).ToList();
            var pendingReducer = new PendingReducerCall
            {
                Reducer = typedReducer,
            };
            pendingReducerCalls[requestId] = pendingReducer;
            webSocket.Send(new ClientMessage.CallReducer(new CallReducer(
                requestId,
                0, // v2 parity with Rust SDK: always CallReducerFlags::Default.
                args.ReducerName,
                encodedArgs
            )));
        }

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Derive your args type from SpacetimeDB.Reducer (extend the generated partial reducer) instead of only implementing IReducerArgs
  2. Call reducers through the generated wrappers (conn.Reducers.MyReducer(...)) rather than driving InternalCallReducer yourself
  3. Regenerate module bindings so args types come from the same version as the client SDK

Example fix

// before
public readonly struct MyArgs : IReducerArgs { public string ReducerName => "my_reducer"; ... }
conn.InternalCallReducer(new MyArgs()); // throws: not a Reducer

// after
public partial struct MyReducer : SpacetimeDB.Reducer { ... }
conn.Reducers.MyReducer(new MyArgs { ... }); // generated wrapper passes a Reducer-derived type
Defensive patterns

Strategy: type-guard

Type guard

static bool IsReducerCall(object args) => args is SpacetimeDB.Reducer;

Try / catch

try { conn.Reducers.MyReducer(args); }
catch (InvalidOperationException e) when (e.Message.Contains("not assignable to"))
{ /* args type must derive from SpacetimeDB.Reducer — regenerate or fix the partial */ }

Prevention

When it happens

Trigger: Calling a reducer with a custom args class that implements IReducerArgs (or IStructuralReadWrite + ReducerName) but does not derive from SpacetimeDB.Reducer; invoking IDbConnection.InternalCallReducer directly with such a type; mixing args types generated against a different SDK/module version.

Common situations: Hand-rolling reducer argument types instead of using the generated partial classes; upgrading the module without regenerating bindings so base types diverge.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/1d68e86ce347ff08. Report an issue: GitHub.