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
- Verify the endpoint URL includes the full API path (default https://api.ht-ml.app/v1/sites).
- Inspect e.__cause__ (a json.JSONDecodeError) — its lineno/colno plus a manual curl of the endpoint reveals what body came back.
- 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
- Always use the full API path in endpoint= (default .../v1/sites), never the bare host.
- Inspect e.__cause__ (JSONDecodeError) to see where the body fails to parse.
- In tests, stub the endpoint with a JSON-returning handler, not an HTML page.
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
- publish endpoint returned unexpected JSON response
- {status}: {message}
- {exc.reason}
- {exc}
- publish endpoint response did not include a valid url
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/df4ef49d3cc70eed.
Report an issue: GitHub.