{"record":{"id":"44c5038bafe622c2","repo":"neondatabase/neon","slug":"could-not-parse-spec","errorCode":null,"errorMessage":"could not parse spec","messagePattern":"could not parse spec","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"compute_tools/src/configurator.rs","lineNumber":107,"sourceCode":"                        Err(anyhow::anyhow!(\n                            \"could not open config file at path: {}\",\n                            config_path.to_string_lossy()\n                        ))\n                    }\n                } else if let Some(control_plane_uri) = &compute.params.control_plane_uri {\n                    get_config_from_control_plane(control_plane_uri, &compute.params.compute_id)\n                } else {\n                    Err(anyhow::anyhow!(\"config_path_test_only is not set\"))\n                };\n\n            // Parse any received ComputeSpec and transpose the result into a Result<Option<ParsedSpec>>.\n            let parsed_spec_result: Result<Option<ParsedSpec>> =\n                get_config_result.and_then(|config| {\n                    if let Some(spec) = config.spec {\n                        if let Ok(pspec) = ParsedSpec::try_from(spec) {\n                            Ok(Some(pspec))\n                        } else {\n                            Err(anyhow::anyhow!(\"could not parse spec\"))\n                        }\n                    } else {\n                        Ok(None)\n                    }\n                });\n\n            let new_status: ComputeStatus;\n            match parsed_spec_result {\n                // Control plane (HCM) returned a spec and we were able to parse it.\n                Ok(Some(pspec)) => {\n                    {\n                        let mut state = compute.state.lock().unwrap();\n                        // Defensive programming to make sure this thread is indeed the only one that can move the compute\n                        // node out of the `RefreshConfiguration` state. Would be nice if we can encode this invariant\n                        // into the type system.\n                        assert_eq!(state.status, ComputeStatus::RefreshConfiguration);\n\n                        if state","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/compute_tools/src/configurator.rs#L89-L125","documentation":"A ComputeSpec arrived and ParsedSpec::try_from(spec) failed, so the configurator replaces it with the generic 'could not parse spec'. The real cause is one of the required-field/parse errors inside try_from (missing pageserver connstring, safekeepers, tenant/timeline id, or a malformed UUID). Note the code uses 'if let Ok', which discards the underlying anyhow error - the single biggest debugging obstacle here.","triggerScenarios":"get_config_result carries a config whose spec fails ParsedSpec::try_from: any of errors 3-6 (missing neon.pageserver_connstring / neon.safekeepers / neon.tenant_id / neon.timeline_id) or 'invalid tenant id' / 'invalid timeline id' formatting, or later try_from invariants.","commonSituations":"Control plane and compute_ctl version skew producing incomplete specs; new validation added to try_from rejecting previously-accepted specs; hand-crafted specs for local testing; JSON field renames between versions.","solutions":["Propagate the real error: replace 'if let Ok(pspec)' with a match that chains {:#} of the underlying error - the message then tells you which field is the culprit","Independently run the spec through ParsedSpec::try_from (unit test / small binary) to see the actual failure","Fix the spec per the revealed message: add the missing GUCs/fields or correct malformed UUIDs","Check for control-plane vs compute_ctl version skew and align them"],"exampleFix":"// before\nif let Ok(pspec) = ParsedSpec::try_from(spec) { Ok(Some(pspec)) } else { Err(anyhow!(\"could not parse spec\")) }\n// after\nmatch ParsedSpec::try_from(spec) {\n    Ok(pspec) => Ok(Some(pspec)),\n    Err(e) => Err(anyhow!(\"could not parse spec: {e:#}\")),\n}","handlingStrategy":"validation","validationCode":"// Reuse the parser as a validator: run try_from and keep the real error\nlet probe = ParsedSpec::try_from(spec.clone());\nif let Err(e) = probe { anyhow::bail!(\"spec will fail to parse: {e:#}\"); }","typeGuard":"fn is_parseable_spec(spec: &ComputeSpec) -> bool {\n    ParsedSpec::try_from(spec.clone()).is_ok()\n}","tryCatchPattern":"// Propagate the source error chain instead of the generic message\nmatch ParsedSpec::try_from(spec) {\n    Ok(pspec) => Ok(Some(pspec)),\n    Err(e) => Err(anyhow!(\"could not parse spec: {e:#}\")),\n}","preventionTips":["Never swallow the inner error: chain it with {:#} so errors 3-6 are visible directly","Contract-test control-plane spec generation against ParsedSpec::try_from on every release","For local specs, validate required GUCs (pageserver_connstring, safekeepers, tenant_id, timeline_id) before launch"],"tags":["rust","compute-ctl","spec","parsing","error-swallowing","configuration"],"backgroundTag":"schema-validation-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}