{"record":{"id":"2504be3c2fd57a33","repo":"BerriAI/litellm","slug":"mavvrik-api-endpoint-host-must-be-a-mavvrik-domain","errorCode":null,"errorMessage":"MAVVRIK_API_ENDPOINT host must be a Mavvrik domain (e.g. https://api.mavvrik.dev/<tenant_id>)","messagePattern":"MAVVRIK_API_ENDPOINT host must be a Mavvrik domain \\(e\\.g\\. https://api\\.mavvrik\\.dev/<tenant_id>\\)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/integrations/focus/destinations/mavvrik_destination.py","lineNumber":36,"sourceCode":"    get_async_httpx_client,\n    httpxSpecialProvider,\n)\n\nfrom .base import FocusDestination, FocusTimeWindow\n\n_MAVVRIK_ALLOWED_SUFFIXES: Final = (\".mavvrik.dev\", \".mavvrik.ai\", \".mavvrik.app\")\n\n# GCS requires intermediate chunks to be a multiple of 256 KB.\n# 8 MB gives a good balance between round-trips and memory pressure.\n_GCS_CHUNK_SIZE: Final = 8 * 1024 * 1024  # 8 MB\n\n\ndef _validate_api_endpoint(api_endpoint: str) -> None:\n    if not api_endpoint.startswith(\"https://\"):\n        raise ValueError(\"MAVVRIK_API_ENDPOINT must be an HTTPS URL\")\n    hostname: Final = (urlparse(api_endpoint).hostname or \"\").lower()\n    if not any(hostname.endswith(suffix) for suffix in _MAVVRIK_ALLOWED_SUFFIXES):\n        raise ValueError(\n            \"MAVVRIK_API_ENDPOINT host must be a Mavvrik domain (e.g. https://api.mavvrik.dev/<tenant_id>)\"\n        )\n\n\ndef _validate_gcs_url(url: str, label: str) -> None:\n    parsed: Final = urlparse(url)\n    if parsed.scheme != \"https\":\n        raise ValueError(f\"Mavvrik FOCUS destination: {label} must be HTTPS, got scheme '{parsed.scheme}'\")\n    hostname: Final = (parsed.hostname or \"\").lower()\n    if not (hostname == \"storage.googleapis.com\" or hostname.endswith(\".storage.googleapis.com\")):\n        raise ValueError(\n            f\"Mavvrik FOCUS destination: {label} must be a GCS endpoint (storage.googleapis.com), got '{hostname}'\"\n        )\n\n\nclass FocusMavvrikDestination(FocusDestination):\n    \"\"\"Upload FOCUS CSV exports to Mavvrik via GCS signed URL.\"\"\"\n","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/BerriAI/litellm/blob/6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d/litellm/integrations/focus/destinations/mavvrik_destination.py#L18-L54","documentation":"The second Mavvrik endpoint check parses the https URL's hostname and requires it to end with one of the allowed suffixes: .mavvrik.dev, .mavvrik.ai, or .mavvrik.app. This is an SSRF/typo guard ensuring the API key is only sent to genuine Mavvrik domains.","triggerScenarios":"Passing a valid https URL whose host is not a Mavvrik domain, e.g. https://api.mavvrik.example.com, https://mavvrik-dev.com/tenant, https://localhost:8080, or https://evil.dev (note: 'evil.dev' fails because suffix matching requires '.mavvrik.dev', not just '.dev').","commonSituations":"Custom/private Mavvrik deployments on a company domain; typos in the domain (mavvrik vs mavrik); trying to point the destination at a mock/staging server you control.","solutions":["Use an official Mavvrik domain: https://api.mavvrik.dev/<tenant_id> (or .mavvrik.ai / .mavvrik.app).","Verify the exact tenant URL from the Mavvrik dashboard's connection instructions.","For mocking in tests, monkeypatch FocusMavvrikDestination or the allowed-suffixes tuple rather than the endpoint value."],"exampleFix":"# before\nMAVVRIK_API_ENDPOINT=https://metrics.internal.acme.com/tenant\n\n# after\nMAVVRIK_API_ENDPOINT=https://api.mavvrik.dev/acme-tenant","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\nallowed = (\".mavvrik.dev\", \".mavvrik.ai\", \".mavvrik.app\")\nhost = (urlparse(endpoint).hostname or \"\").lower()\nif not any(host.endswith(s) for s in allowed):\n    raise SystemExit(f\"MAVVRIK_API_ENDPOINT host '{host}' is not a Mavvrik domain\")","typeGuard":"from urllib.parse import urlparse\n\ndef is_mavvrik_domain(url: str) -> bool:\n    host = (urlparse(url).hostname or \"\").lower()\n    return any(host.endswith(s) for s in (\".mavvrik.dev\", \".mavvrik.ai\", \".mavvrik.app\"))","tryCatchPattern":"try:\n    dest = FocusMavvrikDestination(prefix=p, config=cfg)\nexcept ValueError as e:\n    if \"Mavvrik domain\" in str(e):\n        raise ConfigError(f\"Endpoint '{endpoint}' is not an official Mavvrik host\") from e\n    raise","preventionTips":["Copy the exact tenant endpoint URL from the Mavvrik dashboard connection page.","Treat domain-check failures as config typos first: check mavvrik spelling."],"tags":["focus","mavvrik","validation","security","ssrf"],"backgroundTag":null,"analyzedSha":"6c2dcb801bf2b75c18f1bb24140e7cf57465cc4d","analyzedAt":"2026-08-15T07:12:03.035Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}