Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,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 mcp start` now stops without an error when the MCP client closes the connection while the server writes a reply. Previously, the command stopped with `BrokenPipeError` and exit status 1.
- The CLI now exits with status 1 when you interrupt it with Ctrl+C after the reader of its stderr has closed the pipe. Before, the CLI sent an error report and exited with status 120.
- `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.
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