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
- Confirm you are hitting the documented endpoint version (default /v1/sites) that returns an object with a 'url' field.
- If using a custom endpoint/mock, return {"url": "https://..."} as the top-level object.
- 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
- Pin custom endpoints to the API version whose response is a JSON object with 'url'.
- In mocks, always return a dict shaped like the provider: {"url": "https://..."}.
- After provider updates, curl the endpoint once and assert isinstance(json, dict) before shipping.
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
- publish endpoint returned non-JSON response
- publish endpoint response did not include a valid url
- {label.capitalize()} file {file_path} must be a top-level JS
- Judgments file {path} must carry a top-level \"judgments\" l
- Angles file {path} must carry a top-level \"angles\" list.
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/369746f563135836.
Report an issue: GitHub.