mvanhorn/last30days-skill · error · HtmlPublishError

publish endpoint returned unexpected JSON response

Error message

publish endpoint returned unexpected JSON response

What it means

Raised by publish_html when the 2xx response body parses as JSON but is not an object — e.g. a JSON array or a bare string/number. The contract requires a top-level object because the next step reads result.get('url'); a non-dict would either crash or signal a semantically wrong response, so it is rejected explicitly with HtmlPublishError.

Source

Thrown at skills/last30days/scripts/lib/html_publish.py:66

    )
    open_fn = opener or urlopen
    try:
        with open_fn(request, timeout=timeout) as response:
            body = response.read().decode("utf-8")
    except HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        raise HtmlPublishError(_error_message(exc.code, detail)) from exc
    except URLError as exc:
        raise HtmlPublishError(str(exc.reason)) from exc
    except OSError as exc:
        raise HtmlPublishError(str(exc)) from exc

    try:
        result = json.loads(body)
    except json.JSONDecodeError as exc:
        raise HtmlPublishError("publish endpoint returned non-JSON response") from exc
    if not isinstance(result, dict):
        raise HtmlPublishError("publish endpoint returned unexpected JSON response")

    url = result.get("url")
    if not isinstance(url, str) or not url.startswith("https://"):
        raise HtmlPublishError("publish endpoint response did not include a valid url")
    return result


def publish_html_documents(
    documents: Mapping[str, str],
    *,
    password: str | None = None,
    endpoint: str = DEFAULT_ENDPOINT,
    opener: Callable[..., Any] | None = None,
    timeout: int = 30,
) -> HtmlPublishBatchResult:
    """Publish a named set of documents, preserving caller order in results."""
    results = HtmlPublishBatchResult()
    for name, content in documents.items():

View on GitHub (pinned to c7460f6114)

Solutions

  1. Confirm you are hitting the documented endpoint version (default /v1/sites) that returns an object with a 'url' field.
  2. If using a custom endpoint/mock, return {"url": "https://..."} as the top-level object.
  3. Print the parsed type (json.loads(body) -> type) from a manual curl to pin down the mismatch.

Example fix

# mock before
handle = lambda req: json.dumps([{"url": "https://x"}]).encode()

# mock after
handle = lambda req: json.dumps({"url": "https://x"}).encode()
Defensive patterns

Strategy: try-catch

Validate before calling

import json

def response_is_object(body: str) -> bool:
    try:
        return isinstance(json.loads(body), dict)
    except json.JSONDecodeError:
        return False

Type guard

def is_publish_response(payload: object) -> bool:
    return isinstance(payload, dict) and isinstance(payload.get("url"), str)

Try / catch

try:
    result = publish_html(html)
except HtmlPublishError as e:
    if 'unexpected JSON' in str(e):
        # endpoint returned an array/scalar — contract drift; check endpoint version
        ...

Prevention

When it happens

Trigger: A custom or changed endpoint that returns a JSON array of results or a bare JSON scalar with 200 status; a mock/test server returning json.dumps(["ok"]); an API version bump that wraps the payload differently.

Common situations: Developing against a stub endpoint during integration; the provider ships a v2 that returns a list for batch operations while the client still expects the v1 object shape.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/369746f563135836. Report an issue: GitHub.