bytedance/deer-flow · error · HTTPException

Cannot set env key '{k}' to masked value '***'; provide a re

Error message

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

What it means

400 raised when an MCP config update sends the masked sentinel '***' for an env key that does not exist in the currently stored server config. Masked GET responses are meant to round-trip: existing keys sent back as '***' keep their stored value. A masked value for a brand-new key has nothing to preserve, so it is rejected.

Source

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

    GET (masked) → modify enabled → PUT (masked values sent back).
    This function ensures masked values (``***``) are replaced with the
    real secrets from the current on-disk config.

    ``***`` is only accepted for keys that already exist in *existing*.
    New keys must provide a real value.

    For OAuth secrets, ``None`` means "preserve the existing stored value"
    so masked GET responses can be safely round-tripped. To explicitly clear
    a stored secret, clients may send an empty string, which is converted
    to ``None`` before persisting.
    """
    merged_env = {}
    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

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Provide the real secret value for any env key not already stored
  2. Only send '***' for keys that exist in the stored config (those are preserved on merge)
  3. Maintain the authoritative unmasked config outside the API and diff against it, not against masked GET output

Example fix

# before
env: {"EXISTING_KEY": "***", "NEW_KEY": "***"}  # NEW_KEY -> 400
# after
env: {"EXISTING_KEY": "***", "NEW_KEY": "real-secret-value"}
Defensive patterns

Strategy: validation

Validate before calling

function resolveMaskedEnv(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 env key '${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 === '***'; }

Try / catch

null

Prevention

When it happens

Trigger: PUT to /api/mcp/config adding env: {"API_KEY": "***"} when the stored server has no API_KEY; re-submitting a masked GET body but renaming or adding env keys; creating a new server by templating another server's masked response.

Common situations: Config round-trip edit flows where the user adds a new secret field but leaves the masked placeholder; automation that merges masked snapshots into new server definitions.

Related errors


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