odysseus-dev/odysseus · error · HTTPException
Invalid base64 image payload: {e}
Error message
Invalid base64 image payload: {e} What it means
Raised in /v1/images/harmonize of scripts/mlx_image_server.py with HTTP 400 when base64.b64decode fails on the request's image (or mask) payload. The handler strips an optional data-URL prefix with split(',', 1)[-1] then decodes; any binascii.Error (bad padding, non-alphabet chars) or TypeError is chained into the 400 with its message.
Source
Thrown at scripts/mlx_image_server.py:429
out_images.append({"b64_json": base64.b64encode(out_path.read_bytes()).decode("ascii")})
return {"created": 0, "data": out_images}
raise HTTPException(
422,
"This MLX image endpoint supports text-to-image generation only. "
"Use /v1/images/generations, or serve an edit/img2img-capable model.",
)
@app.post("/v1/images/harmonize")
def harmonize_image(req: HarmonizeRequest):
active_model = _args.model
if _is_lama_inpaint(active_model) or _is_ddcolor(active_model):
try:
image_raw = base64.b64decode(req.image.split(",", 1)[-1])
mask_b64 = req.body_mask or req.mask
mask_raw = base64.b64decode(mask_b64.split(",", 1)[-1]) if mask_b64 else None
except Exception as e:
raise HTTPException(400, f"Invalid base64 image payload: {e}") from e
with tempfile.TemporaryDirectory(prefix="odysseus-mlx-harmonize-") as td:
out_path = Path(td) / "image.png"
if _is_ddcolor(active_model):
_run_ddcolor_bridge(active_model, image_raw, out_path)
else:
_run_inpaint_bridge(active_model, image_raw, mask_raw, out_path)
if not out_path.exists():
raise HTTPException(500, f"MLX Swift bridge completed but did not write {out_path}")
return {"image": base64.b64encode(out_path.read_bytes()).decode("ascii")}
raise HTTPException(
422,
"This MLX image endpoint supports text-to-image generation only. "
"Use /v1/images/generations, or serve an edit/img2img-capable model.",
)
def main() -> None:
global _argsView on GitHub (pinned to f9235ebbf1)
Solutions
- Validate client-side before sending: base64.b64decode(payload.split(',')[-1], validate=True) round-trips
- Send a proper data URL or pure base64 string of the PNG, no newlines or URL-encoding
- If sending a mask, apply the same checks to body_mask/mask
- Log the payload length/prefix on failure to spot URL-vs-base64 mistakes
Example fix
# before
{"image": "https://cdn/img.png"} # 400 Invalid base64
# after
import base64
{"image": "data:image/png;base64," + base64.b64encode(open('img.png','rb').read()).decode()} Defensive patterns
Strategy: validation
Validate before calling
import base64, binascii
def to_data_url(png_path: str) -> str:
b64 = base64.b64encode(open(png_path, 'rb').read()).decode('ascii')
base64.b64decode(b64, validate=True) # self-check
return 'data:image/png;base64,' + b64 Type guard
def is_valid_b64_payload(s: str) -> bool:
try:
base64.b64decode(s.split(',', 1)[-1], validate=True)
return True
except Exception:
return False Try / catch
try:
post_harmonize(image=payload)
except HTTPException as e:
if e.status_code == 400 and 'Invalid base64' in str(e.detail):
payload = encode_file_properly(png_path); retry_once()
else:
raise Prevention
- Always build payloads with base64.b64encode, never hand-copied strings
- Strip data-URL prefixes yourself and log payload length on error
- Never URL-encode or wrap base64 across newlines in JSON
When it happens
Trigger: POST /v1/images/harmonize with req.image that is not valid base64: plain URL to an image, raw binary bytes JSON-encoded incorrectly, base64 with newlines/spaces from terminal copy-paste, or a data URL whose payload after the comma was truncated; same for body_mask/mask fields.
Common situations: Frontend sends encodeURIComponent'd raw binary instead of base64; copy-paste from 'base64 -w0' vs wrapped output; data URI missing its comma so the whole 'data:image/png;base64,...' string is decoded; trailing '=' padding stripped by a transport layer.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/8b27f801d70353f4.
Report an issue: GitHub.