clockworklabs/SpacetimeDB · error

ERROR: Skipping procedure registration '%s' because circular

Error message

ERROR: Skipping procedure registration '%s' because circular reference error is set

What it means

RegisterProcedure skips an HTTP-callable procedure registration because g_circular_ref_error is already set by a prior type-cycle detection during static module build. The procedure's static_assert on ProcedureContext is not what failed — the module-wide poison flag is. The module will subsequently fail preinit_99 validation, so the procedure never becomes callable.

Source

Thrown at crates/bindings-cpp/include/spacetimedb/internal/v10_builder.h:535

        uint32_t index = static_cast<uint32_t>(is_anonymous ? (GetAnonymousViewHandlerCount() - 1) : (GetViewHandlerCount() - 1));

        RawViewDefV10 view_def{
            view_name,
            index,
            is_public,
            is_anonymous,
            ProductType{},
            return_type,
        };
        UpsertView(view_def);
    }

    template<typename Func>
    void RegisterProcedure(const std::string& procedure_name,
                           Func func,
                           const std::vector<std::string>& param_names = {}) {
        if (g_circular_ref_error) {
            std::fprintf(stderr, "ERROR: Skipping procedure registration '%s' because circular reference error is set\n",
                        procedure_name.c_str());
            return;
        }
        using traits = function_traits<Func>;
        using ReturnType = typename traits::result_type;
        static_assert(traits::arity > 0, "Procedure must have at least one parameter (ProcedureContext)");
        if constexpr (traits::arity > 0) {
            using FirstParamType = std::remove_cv_t<std::remove_reference_t<typename traits::template arg_t<0>>>;
            static_assert(std::is_same_v<FirstParamType, ProcedureContext>,
                "First parameter of procedure must be ProcedureContext");
        }

        std::function<std::vector<uint8_t>(ProcedureContext&, BytesSource)> handler;
        if constexpr (traits::arity == 1) {
            handler = [func](ProcedureContext& ctx, BytesSource) -> std::vector<uint8_t> {
                auto result = func(ctx);
                IterBuf buf = IterBuf::take();
                {

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Resolve the circular reference reported by '[CIRCULAR REFERENCE DETECTED]' earlier in the build/runtime log
  2. Make recursive DTOs acyclic: children become std::vector<uint64_t> or flat lists instead of nested values
  3. Rebuild and verify the procedure is listed in the published module
  4. If multiple 'Skipping ...' lines appear, they share this single root cause — fix it once

Example fix

// before — recursive DTO blocks procedure registration
struct TreeDto { std::string label; std::vector<TreeDto> kids; };
// after
struct TreeDto { std::string label; std::vector<uint32_t> kid_ids; };
Defensive patterns

Strategy: validation

Validate before calling

#include <spacetimedb/internal/module_type_registration.h>
TEST(Module, ProceduresRegistered) {
    EXPECT_FALSE(SpacetimeDB::Internal::g_circular_ref_error);
}

Prevention

When it happens

Trigger: An ST_PROCEDURE macro (procedure_macros.h) instantiates a registration that calls getV10Builder().RegisterProcedure('<name>', fn) in a module whose typespace also contains a recursive type — often a procedure parameter or shared DTO that contains itself via Vec/Option. The flag is set during type building, before the procedure registration runs.

Common situations: Adding a JSON/HTTP procedure whose payload struct is recursive (e.g. a tree DTO); a refactor that makes two DTOs reference each other in a module that already exposes procedures; moving types between headers so registration order changes.

Related errors


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