Hmbown/CodeWhale · error

unsafe MCP HTTP header '{name}'

Error message

unsafe MCP HTTP header '{name}'

What it means

insert_header (oauth.rs:592) validates every header built by build_default_headers with super::headers::is_safe_custom_header (headers.rs:38), which rejects three cases: empty/whitespace-only header names, names that override the protocol-framing headers Accept or Content-Type, and values containing CR or LF (response-splitting defense). Unlike apply_safe_custom_headers, which only warns and skips, the default-headers builder hard-fails the whole call because the map is constructed up front.

Source

Thrown at crates/tui/src/mcp/oauth.rs:592

    env_headers: &HashMap<String, String>,
) -> Result<HeaderMap> {
    let mut headers = HeaderMap::new();
    for (name, value) in http_headers {
        insert_header(&mut headers, name, value)?;
    }
    for (name, env_var) in env_headers {
        if let Ok(value) = std::env::var(env_var)
            && !value.trim().is_empty()
        {
            insert_header(&mut headers, name, &value)?;
        }
    }
    Ok(headers)
}

fn insert_header(headers: &mut HeaderMap, name: &str, value: &str) -> Result<()> {
    if !super::headers::is_safe_custom_header(name, value) {
        bail!("unsafe MCP HTTP header '{name}'");
    }
    let name = HeaderName::from_bytes(name.as_bytes())
        .with_context(|| format!("invalid MCP HTTP header name '{name}'"))?;
    let value = HeaderValue::from_str(value).with_context(|| "invalid MCP HTTP header value")?;
    headers.insert(name, value);
    Ok(())
}

pub fn apply_default_headers(
    builder: reqwest::ClientBuilder,
    headers: &HeaderMap,
) -> reqwest::ClientBuilder {
    if headers.is_empty() {
        builder
    } else {
        builder.default_headers(headers.clone())
    }
}

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Strip CR/LF from environment values before they reach config: tr -d '\r\n' < token.txt
  2. Rename or remove empty header keys and any Accept/Content-Type overrides — the transport sets those itself
  3. Validate the header map with the same three rules before passing config to the OAuth/login path

Example fix

# before
export MY_SERVER_TOKEN="$(cat token.txt)"   # trailing \n in value
[mcp.servers.my-server.env-headers]
X-Api-Token = "MY_SERVER_TOKEN"
Accept = "text/event-stream"

# after
export MY_SERVER_TOKEN="$(tr -d '\r\n' < token.txt)"
[mcp.servers.my-server.env-headers]
X-Api-Token = "MY_SERVER_TOKEN"
Defensive patterns

Strategy: validation

Validate before calling

fn is_safe_custom_header(key: &str, value: &str) -> bool {
    let trimmed = key.trim();
    !trimmed.is_empty()
        && !trimmed.eq_ignore_ascii_case("accept")
        && !trimmed.eq_ignore_ascii_case("content-type")
        && !value.contains('\r')
        && !value.contains('\n')
}

for (k, v) in &server.headers {
    assert!(is_safe_custom_header(k, v), "unsafe header {k:?}");
}

Try / catch

match build_default_headers(&server.headers, &server.env_headers) {
    Err(err) if err.to_string().contains("unsafe MCP HTTP header") => {
        // strip the named header from config and retry once
    }
    other => other?,
}

Prevention

When it happens

Trigger: An McpServerConfig headers or env_headers entry has an empty key, an `Accept`/`Content-Type` key (any case), or a value with embedded \r or \n. For env_headers the value comes from the environment variable at runtime, so a token file read with a trailing newline triggers it.

Common situations: TOKEN=$(cat token.txt) leaves a trailing newline that lands in a header value; copy-pasted header blocks that set Accept: text/event-stream, fighting the transport's own negotiation; YAML/TOML template with an empty header placeholder key.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/2e996438a50ab455. Report an issue: GitHub.