Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `cloudsmith credential-helper install` and `uninstall` no longer rewrite a config file when they make no change to its data. Previously, the CLI always rewrote a file in a different formatting, such as the tab-indented `config.json` that Docker writes. The command also made a `.bak` copy and reported changes that it did not make.
- When the API host name or the proxy host name does not exist, for example because of a typo in `--api-host`, commands now fail immediately. Previously, they retried for approximately 62 seconds without output. The CLI still retries a DNS failure that the operating system reports as temporary.
- When the CLI waits before it retries a failed connection or a 5xx response, it now writes `Request failed: retrying in N second(s) ...` to stderr. Previously, it waited without output.
- When the API host returns an HTML page instead of an API response, the CLI now shows an error and exits with code 1. Examples are a proxy login page, a captive portal, or an `--api-host` that points to a website. Before, commands such as `entitlements list`, `check service`, and `metadata list` stopped with an unexpected error and sent an error report.
- `cloudsmith check rates` no longer stops with `TypeError` when the API does not give a value for an interval, limit, remaining count, reset time or throttled status. The command now shows `None` for that value.

## [1.30.0] - 2026-10-22
Expand Down
30 changes: 30 additions & 0 deletions cloudsmith_cli/cli/tests/commands/test_entitlements.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import httpretty
import httpretty.core
import pytest

from ....core.pagination import PageInfo
from ...commands.entitlements import list_ as list_entitlements
from ...commands.entitlements import print_entitlements
from ...commands.main import main
from ...config import Options
from .conftest import FAKE_API_HOST


@pytest.mark.usefixtures("set_api_key_env_var", "set_api_host_env_var")
Expand All @@ -24,6 +28,32 @@ def test_entitlements_list_with_show_all(runner, organization, tmp_repository):
assert "Invalid value for '--show-all'" not in result.output


@httpretty.activate(allow_net_connect=False)
def test_entitlements_list_html_response_fails_cleanly(runner, monkeypatch):
"""A proxy login page instead of the API gives an error, not a crash."""
monkeypatch.setattr(
httpretty.core.fakesock.socket,
"shutdown",
lambda self, how: None,
raising=False,
)
httpretty.register_uri(
httpretty.GET,
"https://api.example.com/entitlements/o/r/",
body="<html>Proxy login</html>",
content_type="text/html",
)

result = runner.invoke(
main,
["entitlements", "list", "o/r", "-k", "key", "--api-host", FAKE_API_HOST],
)

assert result.return_value == 1
assert "Failed to get list of entitlements!" in result.output
assert "HTML page" in result.output


def _make_entitlement(name):
return {
"name": name,
Expand Down
20 changes: 17 additions & 3 deletions cloudsmith_cli/core/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,24 @@ def request(
resp.encoding = resp.apparent_encoding or "utf-8"
rest_resp = RestResponse(resp)

if _preload_content:
logger.debug("response body: %s", rest_resp.data)

if not 200 <= rest_resp.status <= 299:
raise ApiException(http_resp=rest_resp)

_raise_for_html(rest_resp)
Comment thread
Copilot marked this conversation as resolved.
Resolved

if _preload_content:
logger.debug("response body: %s", rest_resp.data)

return rest_resp


def _raise_for_html(rest_resp: RestResponse) -> None:
"""Reject an HTML page, such as a proxy login page, that has a 2xx status."""
content_type = rest_resp.getheader("Content-Type") or ""
media_type = content_type.split(";", 1)[0].strip().lower()
if media_type == "text/html":
raise ApiException(
status=0,
reason="The API host returned an HTML page, not an API response. "
"Check the API host and your proxy settings.",
)
35 changes: 35 additions & 0 deletions cloudsmith_cli/core/tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,41 @@ def test_rejects_an_invalid_timeout(self):

assert raised.value.status == 0

@httpretty.activate(allow_net_connect=False)
def test_html_page_is_not_an_api_response(self):
"""A proxy login page with status 200 raises an API error with status 0."""
initialise_api()
client = RestClient(backoff_factor=0)
url = "https://test.site"
httpretty.register_uri(
"GET", url, body="<html>Proxy login</html>", content_type="text/html"
)

with pytest.raises(ApiException) as raised:
client.request("GET", url)

assert raised.value.status == 0
assert "HTML page" in raised.value.reason

@httpretty.activate(allow_net_connect=False)
def test_non_utf8_html_page_is_not_an_api_response(self):
"""An HTML page that is not UTF-8 raises an API error, not a decode error."""
initialise_api()
client = RestClient(backoff_factor=0)
url = "https://test.site"
httpretty.register_uri(
"GET",
url,
body="<html>Connexion au proxy requise: é</html>".encode("iso-8859-1"),
content_type="text/html; charset=iso-8859-1",
)

with pytest.raises(ApiException) as raised:
client.request("GET", url)

assert raised.value.status == 0
assert "HTML page" in raised.value.reason


class TestCreateRequestsSession:
@pytest.fixture(autouse=True)
Expand Down
Loading