Skip to content

Commit 9f15b40

Browse files
authored
fix: improve error handling when request receives non-JSON response (#1477)
improve error handling when request receives non-JSON response. Closes 1472
1 parent 774781b commit 9f15b40

3 files changed

Lines changed: 53 additions & 10 deletions

File tree

src/blueapi/cli/cli.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from blueapi.client.rest import (
2727
BlueskyRemoteControlError,
2828
InvalidParametersError,
29+
NonJsonResponseError,
2930
ServiceUnavailableError,
3031
UnauthorisedAccessError,
3132
UnknownPlanError,
@@ -513,13 +514,16 @@ def login(obj: dict) -> None:
513514
print("Logged in")
514515
except Exception:
515516
client = BlueapiClient.from_config(config)
516-
if oidc := client.oidc_config:
517-
auth = SessionManager(
518-
oidc, cache_manager=SessionCacheManager(config.auth_token_path)
519-
)
520-
auth.start_device_flow()
521-
else:
522-
print("Server is not configured to use authentication!")
517+
try:
518+
if oidc := client.oidc_config:
519+
auth = SessionManager(
520+
oidc, cache_manager=SessionCacheManager(config.auth_token_path)
521+
)
522+
auth.start_device_flow()
523+
else:
524+
print("Server is not configured to use authentication!")
525+
except NonJsonResponseError as e:
526+
print(str(e))
523527

524528

525529
@main.command(name="logout")

src/blueapi/client/rest.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
from collections.abc import Callable, Mapping
34
from typing import Any, Literal, TypeVar
@@ -45,6 +46,10 @@ class BlueskyRemoteControlError(Exception):
4546
pass
4647

4748

49+
class NonJsonResponseError(Exception):
50+
pass
51+
52+
4853
class BlueskyRequestError(Exception):
4954
def __init__(self, code: int, message: str) -> None:
5055
super().__init__(message, code)
@@ -109,7 +114,7 @@ def _exception(response: requests.Response) -> Exception | None:
109114
if code < 400:
110115
return None
111116
elif code == 404:
112-
return KeyError(str(response.json()))
117+
return KeyError(str(_response_json(response)))
113118
else:
114119
return BlueskyRemoteControlError(code, str(response))
115120

@@ -124,7 +129,7 @@ def _create_task_exceptions(response: requests.Response) -> Exception | None:
124129
return UnknownPlanError()
125130
elif code == 422:
126131
try:
127-
content = response.json()
132+
content = _response_json(response)
128133
return InvalidParametersError(
129134
TypeAdapter(list[ParameterError]).validate_python(
130135
content.get("detail", [])
@@ -138,6 +143,18 @@ def _create_task_exceptions(response: requests.Response) -> Exception | None:
138143
return BlueskyRequestError(code, response.text)
139144

140145

146+
def _response_json(response: requests.Response) -> Any:
147+
try:
148+
return response.json()
149+
except json.decoder.JSONDecodeError as exc:
150+
LOGGER.debug(
151+
f"Invalid json response from <{response.request.url}>: <{response.content}>"
152+
)
153+
raise NonJsonResponseError(
154+
"Response does not contain a valid JSON object"
155+
) from exc
156+
157+
141158
class BlueapiRestClient:
142159
_config: RestConfig
143160
_session_manager: SessionManager | None
@@ -286,7 +303,9 @@ def _request_and_deserialize(
286303
f"but client version is {client_version}. "
287304
f"Some features may not work as expected."
288305
)
289-
deserialized = TypeAdapter(target_type).validate_python(response.json())
306+
deserialized = TypeAdapter(target_type).validate_python(
307+
_response_json(response)
308+
)
290309
return deserialized
291310

292311

tests/unit_tests/cli/test_cli.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,26 @@ def test_login_with_unauthenticated_server(
11381138
assert result.exit_code == 0
11391139

11401140

1141+
@responses.activate
1142+
def test_invalid_json(
1143+
runner: CliRunner,
1144+
config_with_auth: str,
1145+
mock_authn_server: responses.RequestsMock,
1146+
):
1147+
response = responses.add(
1148+
responses.GET,
1149+
"http://localhost:8000/config/oidc",
1150+
body="blah blah",
1151+
status=404,
1152+
)
1153+
1154+
result = runner.invoke(main, ["-c", config_with_auth, "login"])
1155+
1156+
assert response.call_count == 1
1157+
assert "Response does not contain a valid JSON object\n" == result.output
1158+
assert result.exit_code == 0
1159+
1160+
11411161
def test_logout_success(
11421162
runner: CliRunner,
11431163
config_with_auth: str,

0 commit comments

Comments
 (0)