mvanhorn/last30days-skill · error · HtmlPublishError

publish endpoint returned non-JSON response

Error message

publish endpoint returned non-JSON response

What it means

Raised by publish_html when the HTTP request succeeds (2xx) but the response body is not valid JSON. The client sends Accept: application/json and expects a JSON object back; json.JSONDecodeError is caught and re-raised as HtmlPublishError with this fixed message, chained from the original decode error.

Source

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

        headers={"Content-Type": "application/json", "Accept": "application/json"},
        method="POST",
    )
    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."""

View on GitHub (pinned to c7460f6114)

Solutions

  1. Verify the endpoint URL includes the full API path (default https://api.ht-ml.app/v1/sites).
  2. Inspect e.__cause__ (a json.JSONDecodeError) — its lineno/colno plus a manual curl of the endpoint reveals what body came back.
  3. If a proxy rewrites responses, bypass it or fix its configuration.

Example fix

# before
publish_html(html, endpoint="https://api.ht-ml.app")

# after
publish_html(html, endpoint="https://api.ht-ml.app/v1/sites")
Defensive patterns

Strategy: try-catch

Validate before calling

import json, urllib.request

def endpoint_returns_json(endpoint: str) -> bool:
    with urllib.request.urlopen(endpoint) as r:  # GET probe only; POST is the real call
        try:
            json.loads(r.read().decode("utf-8"))
            return True
        except json.JSONDecodeError:
            return False

Try / catch

import json

try:
    publish_html(html, endpoint=endpoint)
except HtmlPublishError as e:
    if isinstance(e.__cause__, json.JSONDecodeError):
        # endpoint returned non-JSON with 200 — verify the URL path, likely hitting an HTML page
        ...

Prevention

When it happens

Trigger: endpoint= pointing at a page that returns HTML (a typo hitting the site root, a 200 status error page), a plain-text response from a misconfigured reverse proxy, or a provider change that starts returning non-JSON on success.

Common situations: Custom endpoint URL missing the API path (e.g. https://api.ht-ml.app instead of /v1/sites) so the server returns the landing page with 200; an interception proxy injecting an HTML banner; the service replaced by something else at the same URL.

Related errors


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