mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-23 01:55:06 +02:00
fix(cli): show the control plane's reason without the HTTP envelope
This commit is contained in:
@@ -1452,7 +1452,9 @@ def _resolve_or_create(
|
||||
|
||||
|
||||
def _needs_a_listener(err: HostBackendError) -> bool:
|
||||
return err.status_code == 400 and _LISTENER_REQUIRED_MARKER in err.message
|
||||
return err.status_code == 400 and _LISTENER_REQUIRED_MARKER in (
|
||||
err.detail or err.message
|
||||
)
|
||||
|
||||
|
||||
def _available_listeners(client: HostBackendClient) -> tuple[Listener, ...]:
|
||||
@@ -1589,7 +1591,7 @@ class CustomerRegistrySource:
|
||||
if _needs_a_listener(err):
|
||||
raise click.UsageError(
|
||||
"This workspace deploys through a listener. Re-run with "
|
||||
f"--listener-id and --k8s-namespace.\n{err.message}"
|
||||
f"--listener-id and --k8s-namespace.\n{err.detail or err.message}"
|
||||
) from None
|
||||
raise
|
||||
return DeployOutcome(
|
||||
|
||||
@@ -103,9 +103,24 @@ def _resources(payload: object) -> list[dict[str, Any]]:
|
||||
class HostBackendError(click.ClickException):
|
||||
"""Raised when the host backend returns an error response."""
|
||||
|
||||
def __init__(self, message: str, status_code: int | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int | None = None,
|
||||
detail: str | None = None,
|
||||
):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def _error_detail(response: httpx.Response) -> str | None:
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
return None
|
||||
detail = body.get("detail") if isinstance(body, dict) else None
|
||||
return detail if isinstance(detail, str) else None
|
||||
|
||||
|
||||
class HostBackendClient:
|
||||
@@ -153,10 +168,12 @@ class HostBackendClient:
|
||||
resp = self._client.request(method, path, json=payload, params=params)
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPStatusError as err:
|
||||
detail = err.response.text or str(err.response.status_code)
|
||||
detail = _error_detail(err.response)
|
||||
reason = detail or err.response.text or str(err.response.status_code)
|
||||
raise HostBackendError(
|
||||
f"{method} {path} failed with status {err.response.status_code}: {detail}",
|
||||
f"{method} {path} failed with status {err.response.status_code}: {reason}",
|
||||
status_code=err.response.status_code,
|
||||
detail=detail,
|
||||
) from None
|
||||
except httpx.TransportError as err:
|
||||
raise HostBackendError(str(err)) from None
|
||||
|
||||
@@ -105,7 +105,7 @@ class ControlPlaneDouble:
|
||||
)
|
||||
if (method, path) == ("POST", "/v2/deployments"):
|
||||
if self.create_error is not None:
|
||||
return httpx.Response(400, text=self.create_error)
|
||||
return httpx.Response(400, json={"detail": self.create_error})
|
||||
return httpx.Response(201, json={"id": CREATED_ID, "tenant_id": "tenant-1"})
|
||||
if path.endswith("/push-token"):
|
||||
if self.push_token_status != 200:
|
||||
@@ -886,6 +886,8 @@ def test_a_control_plane_that_demands_a_listener_names_the_flags(
|
||||
assert "--listener-id" in result.output
|
||||
assert "--k8s-namespace" in result.output
|
||||
assert "listener-1" in result.output
|
||||
assert "{" not in result.output
|
||||
assert "POST /v2/deployments failed" not in result.output
|
||||
|
||||
|
||||
def test_listener_flags_without_push_to_make_no_call_at_all(
|
||||
|
||||
@@ -655,3 +655,34 @@ def test_list_deployments_sends_one_name_filter(call, expected_params):
|
||||
)
|
||||
|
||||
assert seen == expected_params
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("body", "expected"),
|
||||
[
|
||||
pytest.param(
|
||||
{"detail": "Source configuration error: bad listener"},
|
||||
"Source configuration error: bad listener",
|
||||
id="fastapi_detail_is_unwrapped",
|
||||
),
|
||||
pytest.param(
|
||||
{"detail": {"loc": ["body"], "msg": "nope"}},
|
||||
None,
|
||||
id="a_structured_detail_is_left_alone",
|
||||
),
|
||||
pytest.param({"other": "shape"}, None, id="an_unknown_shape_is_left_alone"),
|
||||
],
|
||||
)
|
||||
def test_error_detail_is_readable(body, expected):
|
||||
c = HostBackendClient(
|
||||
"https://api.example.com",
|
||||
"key",
|
||||
transport=httpx.MockTransport(lambda req: httpx.Response(400, json=body)),
|
||||
)
|
||||
|
||||
with pytest.raises(HostBackendError) as error:
|
||||
c.get_deployment("dep-1")
|
||||
|
||||
assert error.value.detail == expected
|
||||
if expected is not None:
|
||||
assert error.value.message.endswith(expected)
|
||||
|
||||
Reference in New Issue
Block a user