PaddlePaddle/PaddleOCR · error · ResultParseError

Malformed JSONL result payload: {e}

Error message

Malformed JSONL result payload: {e}

What it means

ResultParseError from fetch_jsonl in the async API client: the poller downloads the job's result JSONL URL, splits it into lines, and json.loads each non-empty line; any line that is not valid JSON raises with the underlying JSONDecodeError chained. This indicates the downloaded result payload is not the expected line-delimited JSON.

Source

Thrown at paddleocr/_api_client/_async_http.py:163

            return await self._response_data(resp)

    async def get_batch_status(self, batch_id: str) -> Dict[str, Any]:
        await self._ensure_session()
        async with self._session.get(f"{self._jobs_url}/batch/{batch_id}") as resp:
            await self._raise_for_response(resp)
            return await self._response_data(resp)

    async def fetch_jsonl(self, url: str) -> list:
        timeout = aiohttp.ClientTimeout(total=self._timeout)
        async with aiohttp.ClientSession(timeout=timeout) as bare_session:
            async with bare_session.get(url) as resp:
                await self._raise_for_response(resp)
                text = await resp.text()
                try:
                    lines = text.strip().split("\n")
                    return [json.loads(line) for line in lines if line.strip()]
                except json.JSONDecodeError as e:
                    raise ResultParseError(
                        f"Malformed JSONL result payload: {e}"
                    ) from e

    async def _raise_for_response(self, resp) -> None:
        if 200 <= resp.status < 300:
            return
        try:
            body = await resp.json()
            msg = (
                extract_api_message_from_payload(body)
                if isinstance(body, dict)
                else None
            )
            if not msg:
                msg = await resp.text()
        except Exception:
            msg = await resp.text()
        raise_for_status(resp.status, msg)

View on GitHub (pinned to 2661c7c0ef)

Solutions

  1. Fetch the URL manually (curl) and inspect the first lines — HTML/XML means an auth or expiry problem, not corruption.
  2. Retry the poll/parse once; transient truncation or proxy pages often clear.
  3. Upgrade the paddleocr API client if the service changed its result format.
  4. Ensure the result URL is fetched promptly after job completion before any expiry window closes.
Defensive patterns

Strategy: retry

Validate before calling

import json, urllib.request

def jsonl_url_fetchable(url: str) -> bool:
    try:
        with urllib.request.urlopen(url, timeout=30) as r:
            first = r.readline().decode()
            json.loads(first)
            return True
    except Exception:
        return False

Try / catch

from paddleocr._api_client.errors import ResultParseError

for attempt in range(2):
    try:
        data = await http.fetch_jsonl(url)
        break
    except ResultParseError:
        if attempt == 1:
            raise  # persistent: URL likely returns an error page, not JSONL
        await asyncio.sleep(2)

Prevention

When it happens

Trigger: The result URL returns an HTML error page (auth expiry, S3 error XML) with a 200 status; a partially downloaded/truncated file; the endpoint switching format (e.g. a single JSON object or CSV) so one line fails to parse.

Common situations: Pre-signed result URLs expiring between job completion and fetch; proxies injecting content; API version changes altering the result format.

Understand the failure class

Related errors


AI-assisted analysis of PaddlePaddle/PaddleOCR@2661c7c0ef (2026-08-14). Data as JSON: /api/errors/c53540ad3f16b3e7. Report an issue: GitHub.