{"record":{"id":"ccce9925d3229128","repo":"mvanhorn/last30days-skill","slug":"publish-endpoint-response-did-not-include-a-valid","errorCode":null,"errorMessage":"publish endpoint response did not include a valid url","messagePattern":"publish endpoint response did not include a valid url","errorType":"exception","errorClass":"HtmlPublishError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/html_publish.py","lineNumber":70,"sourceCode":"            body = response.read().decode(\"utf-8\")\n    except HTTPError as exc:\n        detail = exc.read().decode(\"utf-8\", errors=\"replace\")\n        raise HtmlPublishError(_error_message(exc.code, detail)) from exc\n    except URLError as exc:\n        raise HtmlPublishError(str(exc.reason)) from exc\n    except OSError as exc:\n        raise HtmlPublishError(str(exc)) from exc\n\n    try:\n        result = json.loads(body)\n    except json.JSONDecodeError as exc:\n        raise HtmlPublishError(\"publish endpoint returned non-JSON response\") from exc\n    if not isinstance(result, dict):\n        raise HtmlPublishError(\"publish endpoint returned unexpected JSON response\")\n\n    url = result.get(\"url\")\n    if not isinstance(url, str) or not url.startswith(\"https://\"):\n        raise HtmlPublishError(\"publish endpoint response did not include a valid url\")\n    return result\n\n\ndef publish_html_documents(\n    documents: Mapping[str, str],\n    *,\n    password: str | None = None,\n    endpoint: str = DEFAULT_ENDPOINT,\n    opener: Callable[..., Any] | None = None,\n    timeout: int = 30,\n) -> HtmlPublishBatchResult:\n    \"\"\"Publish a named set of documents, preserving caller order in results.\"\"\"\n    results = HtmlPublishBatchResult()\n    for name, content in documents.items():\n        try:\n            results[name] = publish_html(\n                content,\n                password=password,","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/html_publish.py#L52-L88","documentation":"Raised by publish_html when the response is a JSON object but its 'url' field is missing, is not a string, or does not start with 'https://'. This is the final validation of the publish contract: the whole point of the call is to obtain a hosted HTTPS URL, so a response without one is treated as a failure even if the status was 200.","triggerScenarios":"Provider returns {\"status\": \"ok\"} without a url; url is null or a number; url uses http:// instead of https://; a partially deployed provider version returns the old response shape; a mock omitting the field.","commonSituations":"Endpoint contract drift after a provider update; custom self-hosted endpoint that returns a relative path like /sites/abc instead of an absolute HTTPS URL; a stub used in tests that forgets the field.","solutions":["Inspect the raw response (curl the endpoint with the same payload) and compare against the expected {\"url\": \"https://...\"} shape.","For self-hosted/custom endpoints, make the response emit the absolute HTTPS URL.","For provider-side drift, pin/roll back to the endpoint version that returns the documented shape."],"exampleFix":"# custom endpoint before\nreturn {\"url\": \"/sites/abc\"}\n\n# custom endpoint after\nreturn {\"url\": \"https://your-host/sites/abc\"}","handlingStrategy":"try-catch","validationCode":"def has_valid_url(payload: dict) -> bool:\n    url = payload.get(\"url\")\n    return isinstance(url, str) and url.startswith(\"https://\")","typeGuard":"def is_valid_publish_url(url: object) -> bool:\n    return isinstance(url, str) and url.startswith(\"https://\")","tryCatchPattern":"try:\n    result = publish_html(html)\nexcept HtmlPublishError as e:\n    if 'valid url' in str(e):\n        # 200 but no usable url — inspect the raw response; likely endpoint/version mismatch\n        ...","preventionTips":["Custom endpoints must return an absolute https:// URL, not a relative path.","When the provider changes its response shape, compare raw curl output against the client's expectation.","Treat a 200-without-url as a contract failure worth alerting on, not a transient error."],"tags":["html-publish","contract","url","endpoint"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}