windmill-labs/windmill · error

{webhook_key} must be a URL string, got {kind}

Error message

{webhook_key} must be a URL string, got {kind}

What it means

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.

Source

Thrown at backend/windmill-common/src/instance_config.rs:1327

    // bool/number/object through as if the key were absent, and the diff below would
    // then persist it — where the HTTP path answers "must be a URL string".
    match desired.get(webhook_key) {
        None | Some(serde_json::Value::Null) => {}
        Some(serde_json::Value::String(s)) if s.trim().is_empty() => {}
        Some(serde_json::Value::String(s)) => crate::global_settings::validate_webhook_base_url(s)
            .map_err(|e| anyhow::anyhow!("{webhook_key}: {e}"))?,
        // Names the JSON kind rather than printing it: this is the last message on
        // this path that could report submitted content, and an object or array could
        // carry a secret into `sync-config` output and operator logs.
        Some(other) => {
            let kind = match other {
                serde_json::Value::Bool(_) => "a boolean",
                serde_json::Value::Number(_) => "a number",
                serde_json::Value::Array(_) => "an array",
                serde_json::Value::Object(_) => "an object",
                _ => "a non-string value",
            };
            return Err(anyhow::anyhow!(
                "{webhook_key} must be a URL string, got {kind}"
            ));
        }
    }

    let diff = diff_global_settings(current, desired, ApplyMode::Replace);
    apply_settings_diff(db, &diff).await?;

    Ok(())
}

/// Apply a settings diff to the global_settings table.
pub async fn apply_settings_diff(
    db: &sqlx::Pool<sqlx::Postgres>,
    diff: &SettingsDiff,
) -> anyhow::Result<()> {
    for (key, value) in &diff.upserts {
        sqlx::query(

View on GitHub (pinned to e474e8803c)

Solutions

  1. Quote the value in YAML (github_app_webhook_base_url: "https://...") so it parses as a string
  2. Replace any non-scalar (list/map) with the intended URL string
  3. Re-run the sync; the fix is purely in the submitted config, no server state to repair
  4. Note the error intentionally hides the value — inspect your local config file to see what was actually submitted

Example fix

// before (YAML type coercion)
github_app_webhook_base_url: 1234        # parsed as a number
// after
github_app_webhook_base_url: "https://windmill.example.com/webhook"
Defensive patterns

Strategy: validation

Validate before calling

// YAML: quote scalar values so parsers don't coerce types
github_app_webhook_base_url: "https://windmill.example.com/webhook"
// pre-flight in code:
let v = desired.get("github_app_webhook_base_url").unwrap();
assert!(v.is_string(), "github_app_webhook_base_url must be a JSON string, got kind {}", json_kind(v));

Type guard

fn json_kind(v: &serde_json::Value) -> &'static str {
    match v {
        serde_json::Value::String(_) => "string",
        serde_json::Value::Bool(_) => "bool",
        serde_json::Value::Number(_) => "number",
        serde_json::Value::Array(_) => "array",
        serde_json::Value::Object(_) => "object",
        serde_json::Value::Null => "null",
    }
}

Try / catch

match sync_global_settings_declarative(&db, &current, &desired).await {
    Err(e) if e.to_string().contains("must be a URL string, got") =>
        anyhow::bail!("your config file has a non-string value for github_app_webhook_base_url; quote it in YAML — {}", e),
    other => other,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/3d1c8f9df5a53f13. Report an issue: GitHub.