From 2f5315da908b7bbaf06ef86f93cc8829c5fa4636 Mon Sep 17 00:00:00 2001 From: alexaustin007 Date: Sat, 19 Jul 2025 23:22:30 +0530 Subject: [PATCH 1/3] fix: handle JSONDecodeError in _handle_response to prevent crashes - Add ServerResponseError and ServerResponseErrorEnum for invalid JSON responses - Wrap response.json() in try/except to catch JSONDecodeError - Return ServerResponseError instead of crashing when API returns empty/malformed JSON - Fixes #62: erratic JSONDecodeError crashes during API polling This prevents application crashes when Dexcom Share API returns empty or invalid JSON responses, allowing graceful error handling and retry logic --- pydexcom/dexcom.py | 58 +++++++++++++++++++++++++--------------------- pydexcom/errors.py | 10 ++++++++ 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/pydexcom/dexcom.py b/pydexcom/dexcom.py index dbdfa84..0a3ae76 100644 --- a/pydexcom/dexcom.py +++ b/pydexcom/dexcom.py @@ -5,6 +5,7 @@ from typing import Any import requests +import json from .const import ( DEFAULT_UUID, @@ -26,6 +27,8 @@ DexcomError, SessionError, SessionErrorEnum, + ServerResponseError, + ServerResponseErrorEnum, ) from .glucose_reading import GlucoseReading from .util import _LOGGER, valid_uuid @@ -109,33 +112,36 @@ def _handle_response(self, response: requests.Response) -> DexcomError | None: :param response: `requests.Response` to parse """ - if response.json(): - _LOGGER.debug("%s", response.json()) - code = response.json().get("Code", None) - message = response.json().get("Message", None) - if code == "SessionIdNotFound": - error = SessionError(SessionErrorEnum.NOT_FOUND) - elif code == "SessionNotValid": - error = SessionError(SessionErrorEnum.INVALID) - elif code == "AccountPasswordInvalid": # defunct - error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) - elif code == "SSO_AuthenticateMaxAttemptsExceeded": - error = AccountError(AccountErrorEnum.MAX_ATTEMPTS) - elif code == "SSO_InternalError": - if message and ( - "Cannot Authenticate by AccountName" in message - or "Cannot Authenticate by AccountId" in message - ): + try: + if response.json(): + _LOGGER.debug("%s", response.json()) + code = response.json().get("Code", None) + message = response.json().get("Message", None) + if code == "SessionIdNotFound": + error = SessionError(SessionErrorEnum.NOT_FOUND) + elif code == "SessionNotValid": + error = SessionError(SessionErrorEnum.INVALID) + elif code == "AccountPasswordInvalid": # defunct error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) - elif code == "InvalidArgument": - if message and "accountName" in message: - error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) - elif message and "password" in message: - error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) - elif message and "UUID" in message: - error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) - elif code and message: - _LOGGER.debug("%s: %s", code, message) + elif code == "SSO_AuthenticateMaxAttemptsExceeded": + error = AccountError(AccountErrorEnum.MAX_ATTEMPTS) + elif code == "SSO_InternalError": + if message and ( + "Cannot Authenticate by AccountName" in message + or "Cannot Authenticate by AccountId" in message + ): + error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) + elif code == "InvalidArgument": + if message and "accountName" in message: + error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) + elif message and "password" in message: + error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) + elif message and "UUID" in message: + error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) + elif code and message: + _LOGGER.debug("%s: %s", code, message) + except json.JSONDecodeError: + error = ServerResponseError(ServerResponseErrorEnum.INVALID_JSON) return error def _validate_region(self, region: Region) -> None: diff --git a/pydexcom/errors.py b/pydexcom/errors.py index 60b6e40..90d61bc 100644 --- a/pydexcom/errors.py +++ b/pydexcom/errors.py @@ -38,6 +38,12 @@ class ArgumentErrorEnum(DexcomErrorEnum): GLUCOSE_READING_INVALID = "JSON glucose reading incorrectly formatted" +class ServerResponseErrorEnum(DexcomErrorEnum): + """`ServerResponseError` strings.""" + + INVALID_JSON = "Invalid or malformed JSON in server response" + + class DexcomError(Exception): """Base class for all `pydexcom` errors.""" @@ -70,3 +76,7 @@ class SessionError(DexcomError): class ArgumentError(DexcomError): """Errors involving `pydexcom` arguments.""" + + +class ServerResponseError(DexcomError): + """Errors involving unexpected or malformed server responses (e.g., JSONDecodeError).""" From 8b89b7d462095cfbcc9441749d138602ad9f07e1 Mon Sep 17 00:00:00 2001 From: gagebenne Date: Sat, 19 Jul 2025 22:16:36 -0400 Subject: [PATCH 2/3] Decode JSON first, then handle errors --- pydexcom/dexcom.py | 88 ++++++++++++++++++++++------------------------ pydexcom/errors.py | 4 ++- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/pydexcom/dexcom.py b/pydexcom/dexcom.py index 0a3ae76..0ce117e 100644 --- a/pydexcom/dexcom.py +++ b/pydexcom/dexcom.py @@ -5,7 +5,6 @@ from typing import Any import requests -import json from .const import ( DEFAULT_UUID, @@ -25,10 +24,10 @@ ArgumentError, ArgumentErrorEnum, DexcomError, - SessionError, - SessionErrorEnum, ServerResponseError, ServerResponseErrorEnum, + SessionError, + SessionErrorEnum, ) from .glucose_reading import GlucoseReading from .util import _LOGGER, valid_uuid @@ -96,53 +95,52 @@ def _post( ) try: + response_json = response.json() response.raise_for_status() - return response.json() except requests.HTTPError as http_error: - error = self._handle_response(response) - if error: - raise error from http_error - _LOGGER.exception("%s", response.text) - raise - - def _handle_response(self, response: requests.Response) -> DexcomError | None: # noqa: C901 - error: DexcomError | None = None + raise self._handle_error_code(response_json) from http_error + except requests.JSONDecodeError as json_error: + _LOGGER.exception("JSON decode error: %s", response.text) + raise ServerResponseError( + ServerResponseErrorEnum.INVALID_JSON + ) from json_error + else: + return response_json + + def _handle_error_code(self, json: dict[str, Any]) -> DexcomError: # noqa: C901, PLR0911 """ - Parse `requests.Response` for `pydexcom.errors.DexcomError`. + Parse `requests.Response` JSON for `pydexcom.errors.DexcomError`. - :param response: `requests.Response` to parse + :param response: `requests.Response` JSON to parse """ - try: - if response.json(): - _LOGGER.debug("%s", response.json()) - code = response.json().get("Code", None) - message = response.json().get("Message", None) - if code == "SessionIdNotFound": - error = SessionError(SessionErrorEnum.NOT_FOUND) - elif code == "SessionNotValid": - error = SessionError(SessionErrorEnum.INVALID) - elif code == "AccountPasswordInvalid": # defunct - error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) - elif code == "SSO_AuthenticateMaxAttemptsExceeded": - error = AccountError(AccountErrorEnum.MAX_ATTEMPTS) - elif code == "SSO_InternalError": - if message and ( - "Cannot Authenticate by AccountName" in message - or "Cannot Authenticate by AccountId" in message - ): - error = AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) - elif code == "InvalidArgument": - if message and "accountName" in message: - error = ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) - elif message and "password" in message: - error = ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) - elif message and "UUID" in message: - error = ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) - elif code and message: - _LOGGER.debug("%s: %s", code, message) - except json.JSONDecodeError: - error = ServerResponseError(ServerResponseErrorEnum.INVALID_JSON) - return error + _LOGGER.debug("%s", json) + code, message = json.get("Code"), json.get("Message") + if code == "SessionIdNotFound": + return SessionError(SessionErrorEnum.NOT_FOUND) + if code == "SessionNotValid": + return SessionError(SessionErrorEnum.INVALID) + if code == "AccountPasswordInvalid": + return AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) + if code == "SSO_AuthenticateMaxAttemptsExceeded": + return AccountError(AccountErrorEnum.MAX_ATTEMPTS) + if code == "SSO_InternalError": # noqa: SIM102 + if message and ( + "Cannot Authenticate by AccountName" in message + or "Cannot Authenticate by AccountId" in message + ): + return AccountError(AccountErrorEnum.FAILED_AUTHENTICATION) + if code == "InvalidArgument": + if message and "accountName" in message: + return ArgumentError(ArgumentErrorEnum.USERNAME_INVALID) + if message and "password" in message: + return ArgumentError(ArgumentErrorEnum.PASSWORD_INVALID) + if message and "UUID" in message: + return ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) + if code and message: + _LOGGER.error("%s: %s", code, message) + return ServerResponseError(ServerResponseErrorEnum.UNKNOWN_CODE) + _LOGGER.error("%s", json) + return ServerResponseError(ServerResponseErrorEnum.UNEXPECTED) def _validate_region(self, region: Region) -> None: if region not in list(Region): diff --git a/pydexcom/errors.py b/pydexcom/errors.py index 90d61bc..f0c9ab5 100644 --- a/pydexcom/errors.py +++ b/pydexcom/errors.py @@ -42,6 +42,8 @@ class ServerResponseErrorEnum(DexcomErrorEnum): """`ServerResponseError` strings.""" INVALID_JSON = "Invalid or malformed JSON in server response" + UNKNOWN_CODE = "Unknown error code in server response" + UNEXPECTED = "Unexpected server response" class DexcomError(Exception): @@ -79,4 +81,4 @@ class ArgumentError(DexcomError): class ServerResponseError(DexcomError): - """Errors involving unexpected or malformed server responses (e.g., JSONDecodeError).""" + """Errors involving unexpected or malformed server responses.""" From f0ddb8511d7b97242c0fadc1a5d42ff567d07bef Mon Sep 17 00:00:00 2001 From: gagebenne Date: Sat, 19 Jul 2025 22:17:48 -0400 Subject: [PATCH 3/3] Shorten ServerResponseError to ServerError --- pydexcom/dexcom.py | 12 +++++------- pydexcom/errors.py | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pydexcom/dexcom.py b/pydexcom/dexcom.py index 0ce117e..49978b3 100644 --- a/pydexcom/dexcom.py +++ b/pydexcom/dexcom.py @@ -24,8 +24,8 @@ ArgumentError, ArgumentErrorEnum, DexcomError, - ServerResponseError, - ServerResponseErrorEnum, + ServerError, + ServerErrorEnum, SessionError, SessionErrorEnum, ) @@ -101,9 +101,7 @@ def _post( raise self._handle_error_code(response_json) from http_error except requests.JSONDecodeError as json_error: _LOGGER.exception("JSON decode error: %s", response.text) - raise ServerResponseError( - ServerResponseErrorEnum.INVALID_JSON - ) from json_error + raise ServerError(ServerErrorEnum.INVALID_JSON) from json_error else: return response_json @@ -138,9 +136,9 @@ def _handle_error_code(self, json: dict[str, Any]) -> DexcomError: # noqa: C901 return ArgumentError(ArgumentErrorEnum.ACCOUNT_ID_INVALID) if code and message: _LOGGER.error("%s: %s", code, message) - return ServerResponseError(ServerResponseErrorEnum.UNKNOWN_CODE) + return ServerError(ServerErrorEnum.UNKNOWN_CODE) _LOGGER.error("%s", json) - return ServerResponseError(ServerResponseErrorEnum.UNEXPECTED) + return ServerError(ServerErrorEnum.UNEXPECTED) def _validate_region(self, region: Region) -> None: if region not in list(Region): diff --git a/pydexcom/errors.py b/pydexcom/errors.py index f0c9ab5..0a6097e 100644 --- a/pydexcom/errors.py +++ b/pydexcom/errors.py @@ -38,8 +38,8 @@ class ArgumentErrorEnum(DexcomErrorEnum): GLUCOSE_READING_INVALID = "JSON glucose reading incorrectly formatted" -class ServerResponseErrorEnum(DexcomErrorEnum): - """`ServerResponseError` strings.""" +class ServerErrorEnum(DexcomErrorEnum): + """`ServerErrorEnum` strings.""" INVALID_JSON = "Invalid or malformed JSON in server response" UNKNOWN_CODE = "Unknown error code in server response" @@ -80,5 +80,5 @@ class ArgumentError(DexcomError): """Errors involving `pydexcom` arguments.""" -class ServerResponseError(DexcomError): +class ServerError(DexcomError): """Errors involving unexpected or malformed server responses."""