BerriAI/litellm · error · ValueError

Unsupported content type: {content_type}

Error message

Unsupported content type: {content_type}

What it means

get_request_body() only accepts two content types for POST: JSON (parsed via _read_request_body) and form/multipart (via get_form_data). Anything else — text/plain, application/xml, application/octet-stream, or a custom type — hits the else branch and raises ValueError('Unsupported content type: {content_type}'). GET and other methods return {} without touching the body.

Source

Thrown at litellm/proxy/common_utils/http_parsing_utils.py:345

            data[key] = [(value.filename, file_content, value.content_type)]
        else:
            # Regular form field
            data[key] = value
    return data


async def get_request_body(request: Request) -> dict[str, Any]:
    """
    Read the request body and parse it as JSON.
    """
    if request.method == "POST":
        content_type: Final = request.headers.get("content-type", "")
        if _is_json_content_type(content_type):
            return await _read_request_body(request)
        elif _is_form_content_type(content_type):
            return await get_form_data(request)
        else:
            raise ValueError(f"Unsupported content type: {content_type}")
    return {}


def extract_nested_form_metadata(form_data: dict[str, Any], prefix: str = "litellm_metadata[") -> dict[str, Any]:
    """
    Extract nested metadata from form data with bracket notation.

    Handles form data that uses bracket notation to represent nested dictionaries,
    such as litellm_metadata[spend_logs_metadata][owner] = "value".

    This is commonly encountered when SDKs or clients send form data with nested
    structures using bracket notation instead of JSON.

    Args:
        form_data: Dictionary containing form data (from request.form())
        prefix: The prefix to look for in form keys (default: "litellm_metadata[")

    Returns:

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Send Content-Type: application/json with a JSON body for POST endpoints.
  2. For file uploads, use multipart/form-data (let the HTTP client set it).
  3. If a non-JSON client must integrate, convert the payload at an upstream adapter before it reaches the proxy.

Example fix

# before
requests.post(url, data="tell me a joke", headers={"Content-Type": "text/plain"})

# after
requests.post(url, json={"model": "gpt-4", "messages": [{"role": "user", "content": "tell me a joke"}]})
Defensive patterns

Strategy: type-guard

Validate before calling

SUPPORTED = ("application/json", "multipart/form-data", "application/x-www-form-urlencoded")

def content_type_supported(ct: str) -> bool:
    return ct.split(";")[0].strip().lower() in SUPPORTED

assert content_type_supported(headers["Content-Type"])

Type guard

def is_supported_content_type(content_type: str) -> bool:
    base = content_type.split(";")[0].strip().lower()
    return base in {"application/json", "multipart/form-data", "application/x-www-form-urlencoded"}

Try / catch

try:
    resp = session.post(url, data=raw, headers={"Content-Type": ct})
    resp.raise_for_status()
except ValueError as e:
    if "Unsupported content type" in str(e):
        resp = session.post(url, json=payload)  # resend as JSON
    else:
        raise

Prevention

When it happens

Trigger: POST to a proxy route that pre-reads the body while sending Content-Type: text/plain or application/xml. Also triggered by clients that omit or mangle the header so it parses as neither JSON nor form.

Common situations: Sending a prompt as raw text with the wrong header. Webhooks that post XML or form-urlencoded data (application/x-www-form-urlencoded is form-parsed only if _is_form_content_type matches; verify your type). A proxy chain rewrites the Content-Type.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/8862562f3a7ab345. Report an issue: GitHub.