{"record":{"id":"715b4f637f40baf4","repo":"clockworklabs/SpacetimeDB","slug":"environment-only-publication-requires-expected-module","errorCode":null,"errorMessage":"environment-only publication requires expected_module_version","messagePattern":"environment-only publication requires expected_module_version","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/core/src/host/host_controller.rs","lineNumber":606,"sourceCode":"    /// [`Self::get_or_launch_module_host`] for details on what this entails).\n    ///\n    /// If the host was running, and the update fails, the previous version of\n    /// the host keeps running.\n    #[tracing::instrument(level = \"trace\", skip_all, err)]\n    #[allow(clippy::too_many_arguments)]\n    pub async fn update_module_host(\n        &self,\n        database: Database,\n        host_type: HostType,\n        replica_id: u64,\n        program_bytes: Box<[u8]>,\n        policy: MigrationPolicy,\n        environment: spacetimedb_lib::environment::EnvironmentUpdate,\n        expected_module_version: Option<spacetimedb_lib::Hash>,\n    ) -> anyhow::Result<UpdateDatabaseResult> {\n        environment.validate()?;\n        let environment_only = program_bytes.is_empty();\n        anyhow::ensure!(\n            !environment_only || expected_module_version.is_some(),\n            \"environment-only publication requires expected_module_version\"\n        );\n        let program = Program::from_bytes(host_type.into(), program_bytes);\n        trace!(\n            \"update module host {}/{}: genesis={} update-to={}\",\n            database.database_identity,\n            replica_id,\n            database.initial_program,\n            program.hash\n        );\n\n        let Ok(mut guard) = self.acquire_write_lock(replica_id).await else {\n            bail!(\"unable to lock database {} for update\", database.database_identity);\n        };\n\n        // `HostController::clone` is fast,\n        // as all of its fields are either `Copy` or wrapped in `Arc`.","sourceCodeStart":588,"sourceCodeEnd":624,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/eddf9f5014579a50d4b67630e28b6e15cad9c4af/crates/core/src/host/host_controller.rs#L588-L624","documentation":"The core's `update_module_host` validates that an environment-only publication (identified by an empty `program_bytes`) always carries an `expected_module_version` hash. This hash implements optimistic concurrency: the caller asserts which module version it read the environment for, preventing the environment update from racing with a concurrent module update. Without it, the core refuses the update via `ensure!`.","triggerScenarios":"Calling `update_module_host` (via the update/publish database path) with empty `program_bytes` (environment-only update) and `expected_module_version: None`; a CLI/server path that constructs an `EnvironmentUpdate` without first fetching the current module hash.","commonSituations":"Tooling or scripts updating only environment variables for a published database but skipping the step of reading the current module version/hash; API clients written before the expected_module_version requirement was introduced.","solutions":["Fetch the database's current module version/hash first and pass it as `expected_module_version`.","If you actually need code changes, supply non-empty `program_bytes` so this is a full module update.","Update the calling client/CLI to a version that includes the module hash in environment-only updates.","Re-read the database state immediately before the call to get a fresh hash and avoid optimistic-concurrency conflicts."],"exampleFix":"// before\nhost_controller.update_module_host(db, host_type, Vec::new(), policy, env_update, None).await?\n// after\nlet expected = get_current_module_hash(db).await?; // e.g. from database metadata\nhost_controller.update_module_host(db, host_type, Vec::new(), policy, env_update, Some(expected)).await?","handlingStrategy":"validation","validationCode":"// rust caller-side check mirroring the core rule\nanyhow::ensure!(\n    !program_bytes.is_empty() || expected_module_version.is_some(),\n    \"environment-only update requires expected_module_version\"\n);","typeGuard":null,"tryCatchPattern":"// rust\nif let Err(e) = update_module_host(..., Vec::new(), policy, env, None).await {\n    if e.to_string().contains(\"environment-only publication requires\") {\n        let hash = fetch_current_module_hash(db).await?;\n        update_module_host(..., Vec::new(), policy, env, Some(hash)).await?;\n    } else { return Err(e); }\n}","preventionTips":["Always read the current module hash immediately before an environment-only update.","Update API clients/CLIs to versions that populate expected_module_version.","Treat the hash as an optimistic-lock token: re-fetch it after any concurrent-update conflict."],"tags":["core","environment","concurrency","validation"],"backgroundTag":"missing-required-argument","analyzedSha":"eddf9f5014579a50d4b67630e28b6e15cad9c4af","analyzedAt":"2026-09-20T12:15:59.611Z","contentChangedAt":"2026-09-20T12:15:59.611Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}