{"record":{"id":"3d1c8f9df5a53f13","repo":"windmill-labs/windmill","slug":"webhook-key-must-be-a-url-string-got-kind","errorCode":null,"errorMessage":"{webhook_key} must be a URL string, got {kind}","messagePattern":"(.+?) must be a URL string, got (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-common/src/instance_config.rs","lineNumber":1327,"sourceCode":"    // bool/number/object through as if the key were absent, and the diff below would\n    // then persist it — where the HTTP path answers \"must be a URL string\".\n    match desired.get(webhook_key) {\n        None | Some(serde_json::Value::Null) => {}\n        Some(serde_json::Value::String(s)) if s.trim().is_empty() => {}\n        Some(serde_json::Value::String(s)) => crate::global_settings::validate_webhook_base_url(s)\n            .map_err(|e| anyhow::anyhow!(\"{webhook_key}: {e}\"))?,\n        // Names the JSON kind rather than printing it: this is the last message on\n        // this path that could report submitted content, and an object or array could\n        // carry a secret into `sync-config` output and operator logs.\n        Some(other) => {\n            let kind = match other {\n                serde_json::Value::Bool(_) => \"a boolean\",\n                serde_json::Value::Number(_) => \"a number\",\n                serde_json::Value::Array(_) => \"an array\",\n                serde_json::Value::Object(_) => \"an object\",\n                _ => \"a non-string value\",\n            };\n            return Err(anyhow::anyhow!(\n                \"{webhook_key} must be a URL string, got {kind}\"\n            ));\n        }\n    }\n\n    let diff = diff_global_settings(current, desired, ApplyMode::Replace);\n    apply_settings_diff(db, &diff).await?;\n\n    Ok(())\n}\n\n/// Apply a settings diff to the global_settings table.\npub async fn apply_settings_diff(\n    db: &sqlx::Pool<sqlx::Postgres>,\n    diff: &SettingsDiff,\n) -> anyhow::Result<()> {\n    for (key, value) in &diff.upserts {\n        sqlx::query(","sourceCodeStart":1309,"sourceCodeEnd":1345,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-common/src/instance_config.rs#L1309-L1345","documentation":"The same declarative settings sync, but for a github_app_webhook_base_url that is present yet not a JSON string at all (bool, number, array, object, or other non-string value). The message deliberately names only the JSON kind and never prints the value, because the submitted content could carry a secret into sync-config output and operator logs.","triggerScenarios":"A YAML/JSON config where the value is unquoted so YAML parses it as bool/number (e.g. true, 1234), or the value is a list/map instead of a scalar string, fed to sync-config or the operator's ConfigMap sync.","commonSituations":"YAML type coercion traps (on/off/yes become booleans, zip codes/ports become numbers), someone putting a nested object where a URL string belongs, templating errors producing a map instead of a string.","solutions":["Quote the value in YAML (github_app_webhook_base_url: \"https://...\") so it parses as a string","Replace any non-scalar (list/map) with the intended URL string","Re-run the sync; the fix is purely in the submitted config, no server state to repair","Note the error intentionally hides the value — inspect your local config file to see what was actually submitted"],"exampleFix":"// before (YAML type coercion)\ngithub_app_webhook_base_url: 1234        # parsed as a number\n// after\ngithub_app_webhook_base_url: \"https://windmill.example.com/webhook\"","handlingStrategy":"validation","validationCode":"// YAML: quote scalar values so parsers don't coerce types\ngithub_app_webhook_base_url: \"https://windmill.example.com/webhook\"\n// pre-flight in code:\nlet v = desired.get(\"github_app_webhook_base_url\").unwrap();\nassert!(v.is_string(), \"github_app_webhook_base_url must be a JSON string, got kind {}\", json_kind(v));","typeGuard":"fn json_kind(v: &serde_json::Value) -> &'static str {\n    match v {\n        serde_json::Value::String(_) => \"string\",\n        serde_json::Value::Bool(_) => \"bool\",\n        serde_json::Value::Number(_) => \"number\",\n        serde_json::Value::Array(_) => \"array\",\n        serde_json::Value::Object(_) => \"object\",\n        serde_json::Value::Null => \"null\",\n    }\n}","tryCatchPattern":"match sync_global_settings_declarative(&db, &current, &desired).await {\n    Err(e) if e.to_string().contains(\"must be a URL string, got\") =>\n        anyhow::bail!(\"your config file has a non-string value for github_app_webhook_base_url; quote it in YAML — {}\", e),\n    other => other,\n}","preventionTips":["Quote all URL settings in YAML to defeat bool/number coercion (on, yes, numeric strings)","Run a JSON-schema/lint pass over declarative settings files before sync","Remember the error hides the value by design — inspect the local config, not logs, to see what was submitted"],"tags":["validation","config","yaml","types"],"backgroundTag":"schema-validation-failed","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}