bytedance/deer-flow · error · HTTPException

Cannot set header '{k}' to masked value '***'; provide a rea

Error message

Cannot set header '{k}' to masked value '***'; provide a real value.

What it means

400 raised under the same masked-value merge logic as env keys, but for HTTP headers: an update sends '***' for a header name that has no stored value on the existing server. Existing masked headers round-trip (stored value preserved); new masked headers cannot.

Source

Thrown at backend/app/gateway/routers/mcp.py:693

    for k, v in incoming.env.items():
        if v == _MASKED_VALUE:
            if k in existing.env:
                merged_env[k] = existing.env[k]
            else:
                raise HTTPException(
                    status_code=400,
                    detail=f"Cannot set env key '{k}' to masked value '***'; provide a real value.",
                )
        else:
            merged_env[k] = v

    merged_headers = {}
    for k, v in incoming.headers.items():
        if v == _MASKED_VALUE:
            if k in existing.headers:
                merged_headers[k] = existing.headers[k]
            else:
                raise HTTPException(
                    status_code=400,
                    detail=f"Cannot set header '{k}' to masked value '***'; provide a real value.",
                )
        else:
            merged_headers[k] = v

    merged_oauth = incoming.oauth
    if incoming.oauth is not None and existing.oauth is not None:
        # None = preserve (masked round-trip), "" = explicitly clear, else = new value
        merged_client_secret = existing.oauth.client_secret if incoming.oauth.client_secret is None else (None if incoming.oauth.client_secret == "" else incoming.oauth.client_secret)
        merged_refresh_token = existing.oauth.refresh_token if incoming.oauth.refresh_token is None else (None if incoming.oauth.refresh_token == "" else incoming.oauth.refresh_token)
        merged_oauth = incoming.oauth.model_copy(
            update={
                "client_secret": merged_client_secret,
                "refresh_token": merged_refresh_token,
            }
        )
    update = {

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Send the actual header value (e.g. the real bearer token) for new header keys
  2. Reserve '***' strictly for keys already present in the stored config
  3. Keep the source of truth for secrets in a vault and inject real values at submit time

Example fix

# before
headers: {"X-Api-Key": "***"}  # new header -> 400
# after
headers: {"X-Api-Key": "actual-key"}
Defensive patterns

Strategy: validation

Validate before calling

function resolveMaskedHeaders(incoming: Record<string, string>, existing: Record<string, string> | undefined): Record<string, string> { const out: Record<string, string> = {}; for (const [k, v] of Object.entries(incoming)) { if (v === '***') { if (!existing || !(k in existing)) throw new Error(`new header '${k}' needs a real value, not the mask`); out[k] = existing[k]; } else out[k] = v; } return out; }

Type guard

function isMasked(v: unknown): v is '***' { return v === '***'; }

Prevention

When it happens

Trigger: PUT to /api/mcp/config with headers: {"Authorization": "***"} on a server that never had an Authorization header; adding a new header while copying masked values from a GET response.

Common situations: Adding auth headers to a previously anonymous HTTP/SSE MCP server via a masked round-trip payload; UI forms pre-filling '***' into new header inputs.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/e4d616041a3193aa. Report an issue: GitHub.