{"record":{"id":"4eb0696a27428b84","repo":"immich-app/immich","slug":"either-image-or-text-must-be-provided","errorCode":null,"errorMessage":"Either image or text must be provided","messagePattern":"Either image or text must be provided","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"machine-learning/immich_ml/main.py","lineNumber":180,"sourceCode":"def ping() -> PlainTextResponse:\n    return PlainTextResponse(\"pong\")\n\n\n@app.post(\"/predict\", dependencies=[Depends(update_state)])\nasync def predict(\n    entries: InferenceEntries = Depends(get_entries),\n    image: bytes | None = File(default=None),\n    text: str | None = Form(default=None),\n) -> Any:\n    if image is not None:\n        decoded = await run(lambda: decode_pil(image))\n        if decoded.width == 0 or decoded.height == 0:\n            raise HTTPException(400, \"Image has zero width or height\")\n        inputs: Image | str = decoded\n    elif text is not None:\n        inputs = text\n    else:\n        raise HTTPException(400, \"Either image or text must be provided\")\n    response = await run_inference(inputs, entries)\n    return ORJSONResponse(response)\n\n\nasync def run_inference(payload: Image | str, entries: InferenceEntries) -> InferenceResponse:\n    outputs: dict[ModelIdentity, Any] = {}\n    response: InferenceResponse = {}\n\n    async def _run_inference(entry: InferenceEntry) -> None:\n        model = await model_cache.get(\n            entry[\"name\"], entry[\"type\"], entry[\"task\"], ttl=settings.model_ttl, **entry[\"options\"]\n        )\n        inputs = [payload]\n        for dep in model.depends:\n            try:\n                inputs.append(outputs[dep])\n            except KeyError:\n                message = f\"Task {entry['task']} of type {entry['type']} depends on output of {dep}\"","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/machine-learning/immich_ml/main.py#L162-L198","documentation":"Raised as HTTPException(400) by the /predict endpoint when neither an 'image' nor a 'text' form field is present. The endpoint requires at least one of the two as the inference payload.","triggerScenarios":"POST /predict with both image and text omitted (e.g. only the 'entries' field supplied), or form-encoding bug that drops both fields.","commonSituations":"Custom client forgets to attach the file/text; a middleware or proxy strips multipart fields; smart-search sends text but it is empty string (note: empty string is still 'not None', so this fires only when the field is truly absent).","solutions":["Ensure the request includes either a non-null image file or a text form field.","Inspect the multipart body actually sent (DevTools/curl -v) to confirm the field names 'image' and 'text'.","If doing CLIP textual search, send text even if empty-looking is not allowed — send an actual non-empty string, or attach an image."],"exampleFix":"# before\ncurl -F 'entries=<json' http://ml/predict   # no image/text\n\n# after\ncurl -F 'entries=<json' -F 'image=@photo.jpg' http://ml/predict\n# or\ncurl -F 'entries=<json' -F 'text=a cat' http://ml/predict","handlingStrategy":"validation","validationCode":"if image is None and (text is None or text == ''):\n    raise HTTPException(400, 'Either image or text must be provided')","typeGuard":"def has_payload(image, text) -> bool:\n    return image is not None or (text is not None and text != '')","tryCatchPattern":"try:\n    resp = await run_inference(inputs, entries)\nexcept HTTPException as e:\n    if e.status_code == 400: client_error()\n    raise","preventionTips":["Always attach exactly one of image/text when calling /predict.","Verify multipart field names with curl -v before integrating."],"tags":["machine-learning","request-validation","predict","fastapi"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}