{"record":{"id":"c28045328668431e","repo":"BerriAI/litellm","slug":"extract-file-data-does-not-accept-bare-str-inputs","errorCode":null,"errorMessage":"extract_file_data does not accept bare str inputs. Pass bytes, an open file handle, a (filename, content) tuple, or a pathlib.Path. To upload a local file from a path, call open(path, 'rb') yourself.","messagePattern":"extract_file_data does not accept bare str inputs\\. Pass bytes, an open file handle, a \\(filename, content\\) tuple, or a pathlib\\.Path\\. To upload a local file from a path, call open\\(path, 'rb'\\) yourself\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/litellm_core_utils/prompt_templates/common_utils.py","lineNumber":789,"sourceCode":"        elif len(file_data) == 3:\n            filename, file_content, content_type = file_data\n        elif len(file_data) == 4:\n            filename, file_content, content_type, file_headers = file_data\n    elif isinstance(file_data, InMemoryFile):\n        filename = file_data.name\n        file_content = file_data\n        content_type = file_data.content_type\n    else:\n        file_content = file_data\n    # Convert content to bytes\n    if isinstance(file_content, str):\n        # Bare string inputs are rejected: when this helper runs in a proxy\n        # request handler the string came from an attacker-controlled form\n        # field, and opening it as a path is an arbitrary file read on the\n        # proxy host. SDK callers who want to upload from a path should\n        # either pass a pathlib.Path (a PathLike instance — see the branch\n        # below) or open the file themselves and pass the handle / bytes.\n        raise ValueError(\n            \"extract_file_data does not accept bare str inputs. Pass bytes, \"\n            \"an open file handle, a (filename, content) tuple, or a \"\n            \"pathlib.Path. To upload a local file from a path, call \"\n            \"open(path, 'rb') yourself.\"\n        )\n    if isinstance(file_content, PathLike):\n        # PathLike (pathlib.Path) is a Python-level type that HTTP form\n        # values can't fabricate. Treat as a local file path for SDK\n        # convenience.\n        if filename is None:\n            filename = Path(file_content).name\n        with open(file_content, \"rb\") as f:\n            content = f.read()\n    elif isinstance(file_content, io.IOBase):\n        # If it's a file-like object\n        # Try to get filename from file handle if not already set\n        if not filename and hasattr(file_content, \"name\"):\n            filename = Path(file_content.name).name","sourceCodeStart":771,"sourceCodeEnd":807,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/litellm_core_utils/prompt_templates/common_utils.py#L771-L807","documentation":"extract_file_data() normalizes file inputs for multimodal messages and rejects bare str inputs. The source comment gives the reason: behind the LiteLLM proxy a str arrives from an attacker-controlled HTTP form field, and interpreting it as a filesystem path would be an arbitrary file read on the proxy host - so strings are refused outright. Accepted inputs are bytes, an open file handle (anything with .read), ContentFile-like objects, a (filename, content) tuple, or a pathlib.Path (a type an HTTP form cannot fabricate).","triggerScenarios":"Passing file content or a path as a plain string in a message part, e.g. {\"type\": \"file\", \"file\": {\"file_content\": \"/etc/passwd\"}} or a base64 str; upgrading litellm to a version with this hardening and re-running code that used to send strings.","commonSituations":"Version upgrade breaking callers that passed paths or raw/base64 strings (deliberate behavior change); proxy file-upload endpoints receiving form-field strings; SDK users migrating from a str-based convenience pattern to the typed contract.","solutions":["Encode text content: file_content.encode(\"utf-8\"); base64.b64decode(...) for base64 strings","For local files pass a handle (open(path, \"rb\")) or a pathlib.Path - both are accepted","Use the tuple form: (\"doc.pdf\", file_bytes)","Proxy operators: keep the contract that clients upload real content, never server-side paths"],"exampleFix":"# before\nmessages = [{\n    \"role\": \"user\",\n    \"content\": [{\"type\": \"file\", \"file\": {\"file_content\": \"@notes.txt\"}}],\n}]  # str -> ValueError\n\n# after\nfrom pathlib import Path\nmessages = [{\n    \"role\": \"user\",\n    \"content\": [{\"type\": \"file\", \"file\": {\"file_content\": Path(\"notes.txt\")}}],\n}]\n# or: {\"file_content\": (\"notes.txt\", Path(\"notes.txt\").read_bytes())}","handlingStrategy":"type-guard","validationCode":"from pathlib import Path\n\ndef normalize_file_input(v):\n    if isinstance(v, str):\n        return v.encode(\"utf-8\")  # treat as text content, never a path\n    return v\n\nmsg = [{\"type\": \"file\", \"file\": {\"file_content\": normalize_file_input(file_content)}}]","typeGuard":"from os import PathLike\n\ndef is_accepted_file_input(v: object) -> bool:\n    if isinstance(v, str):\n        return False\n    return isinstance(v, (bytes, PathLike, tuple)) or hasattr(v, \"read\")","tryCatchPattern":null,"preventionTips":["Never pass filesystem paths as strings through message file fields","Use pathlib.Path or open(path, 'rb') for local uploads - both are accepted","After litellm upgrades, grep your code for string file_content usages before deploying"],"tags":["litellm","security","file-upload","input-validation","arbitrary-file-read"],"backgroundTag":"file-upload-type-validation","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}