zeroclaw-labs/zeroclaw · error · anyhow::Error

Header '{key}' value must be a string, got: {}

Error message

Header '{key}' value must be a string, got: {}

What it means

Thrown by HttpRequestTool::parse_headers (crates/zeroclaw-tools/src/http_request.rs:250) when the headers argument is a JSON object but one of its values is not a JSON string. Every header value must be a string before it can be converted into an http::HeaderValue; numbers, booleans, null, arrays, and objects are rejected, and the error names the offending key. Only after this check are header name/value syntax validated.

Source

Thrown at crates/zeroclaw-tools/src/http_request.rs:250

            "GET" => Ok(reqwest::Method::GET),
            "POST" => Ok(reqwest::Method::POST),
            "PUT" => Ok(reqwest::Method::PUT),
            "DELETE" => Ok(reqwest::Method::DELETE),
            "PATCH" => Ok(reqwest::Method::PATCH),
            "HEAD" => Ok(reqwest::Method::HEAD),
            "OPTIONS" => Ok(reqwest::Method::OPTIONS),
            _ => anyhow::bail!(
                "Unsupported HTTP method: {method}. Supported: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"
            ),
        }
    }

    fn parse_headers(&self, headers: &serde_json::Value) -> anyhow::Result<HeaderMap> {
        let mut result = HeaderMap::new();
        if let Some(obj) = headers.as_object() {
            for (key, value) in obj {
                let Some(str_val) = value.as_str() else {
                    anyhow::bail!("Header '{key}' value must be a string, got: {}", value);
                };
                let header_name = HeaderName::from_str(key)
                    .map_err(|e| anyhow::Error::msg(format!("Invalid header name '{key}': {e}")))?;
                let header_value = HeaderValue::from_str(str_val).map_err(|e| {
                    anyhow::Error::msg(format!("Invalid value for header '{key}': {e}"))
                })?;
                result.insert(header_name, header_value);
            }
        }
        Ok(result)
    }

    fn validate_secret_name(secret_name: &str) -> anyhow::Result<()> {
        if secret_name.is_empty() {
            anyhow::bail!("auth_secret cannot be empty");
        }
        if secret_name.len() > 64 {
            anyhow::bail!("auth_secret must be 64 characters or fewer");

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Stringify every header value in the args: {"X-Retry-Count": "42"}, {"X-Debug": "true"}.
  2. If building the JSON programmatically, map values through to_string()/serde_json::Value::String.
  3. For Authorization values, prefer the auth_secret parameter over literal headers.

Example fix

// before
let args = json!({"url": u, "headers": {"X-Retry-Count": 42, "Content-Type": "application/json"}});

// after
let args = json!({"url": u, "headers": {"X-Retry-Count": "42", "Content-Type": "application/json"}});
Defensive patterns

Strategy: type-guard

Validate before calling

fn headers_values_are_strings(v: &serde_json::Value) -> bool {
    v.as_object().map_or(true, |o| o.values().all(|x| x.is_string()))
}

Type guard

fn headers_all_strings(v: &serde_json::Value) -> bool {
    v.as_object().is_none_or(|o| o.values().all(|x| x.is_string()))
}

Try / catch

let result = tool.execute(args).await?;
if let Some(err) = &result.error {
    if err.contains("value must be a string") {
        // stringify the named header (error includes the key) and retry
    }
}

Prevention

When it happens

Trigger: args.headers = {"X-Retry-Count": 42} or {"X-Debug": true} or {"X-Tags": ["a","b"]}; LLM-generated tool calls that naturally emit JSON numbers for numeric headers; forwarding headers parsed from another JSON payload without stringification; {"Content-Length": 123}.

Common situations: Agent/model tool calls where numeric metadata headers are common (retry counts, IDs, timestamps); integrations copying a JSON config object straight into headers; version upgrades of callers that previously coerced values to strings.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/bf1700f1df82bacd. Report an issue: GitHub.