From bd2098551d4a9dbd0ed562bbacb244b54fc5d76b Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Mon, 24 Feb 2025 19:47:16 -0800 Subject: [PATCH] improved error handling --- socketdev/core/api.py | 56 ++++++++++++++++++++++++++++++++++++----- socketdev/exceptions.py | 43 ++++++++++++++++++++++++------- socketdev/version.py | 2 +- 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/socketdev/core/api.py b/socketdev/core/api.py index 67ee6f1..220ada3 100644 --- a/socketdev/core/api.py +++ b/socketdev/core/api.py @@ -1,8 +1,14 @@ import base64 import requests from socketdev.core.classes import Response -from socketdev.exceptions import APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota, APIResourceNotFound +from socketdev.exceptions import ( + APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota, + APIResourceNotFound, APITimeout, APIConnectionError, APIBadGateway, + APIInsufficientPermissions, APIOrganizationNotAllowed +) from socketdev.version import __version__ +from requests.exceptions import Timeout, ConnectionError +import time class API: @@ -31,23 +37,61 @@ def do_request( } url = f"{self.api_url}/{path}" try: + start_time = time.time() response = requests.request( method.upper(), url, headers=headers, data=payload, files=files, timeout=self.request_timeout ) + request_duration = time.time() - start_time if response.status_code == 401: raise APIAccessDenied("Unauthorized") if response.status_code == 403: - raise APIInsufficientQuota("Insufficient max_quota for API method") + try: + error_message = response.json().get('error', {}).get('message', '') + if "Insufficient permissions for API method" in error_message: + raise APIInsufficientPermissions(error_message) + elif "Organization not allowed" in error_message: + raise APIOrganizationNotAllowed(error_message) + elif "Insufficient max quota" in error_message: + raise APIInsufficientQuota(error_message) + else: + raise APIAccessDenied(error_message or "Access denied") + except ValueError: + # If JSON parsing fails + raise APIAccessDenied("Access denied") if response.status_code == 404: raise APIResourceNotFound(f"Path not found {path}") if response.status_code == 429: - raise APIInsufficientQuota("Insufficient quota for API route") + retry_after = response.headers.get('retry-after') + if retry_after: + try: + seconds = int(retry_after) + minutes = seconds // 60 + remaining_seconds = seconds % 60 + time_msg = f" Quota will reset in {minutes} minutes and {remaining_seconds} seconds" + except ValueError: + time_msg = f" Retry after: {retry_after}" + else: + time_msg = "" + raise APIInsufficientQuota(f"Insufficient quota for API route.{time_msg}") + if response.status_code == 502: + raise APIBadGateway("Upstream server error") if response.status_code >= 400: - raise APIFailure("Bad Request") + raise APIFailure(f"Bad Request: HTTP {response.status_code}") return response + except Timeout: + request_duration = time.time() - start_time + raise APITimeout(f"Request timed out after {request_duration:.2f} seconds") + except ConnectionError as error: + request_duration = time.time() - start_time + raise APIConnectionError(f"Connection error after {request_duration:.2f} seconds: {error}") + except (APIAccessDenied, APIInsufficientQuota, APIResourceNotFound, APIFailure, + APITimeout, APIConnectionError, APIBadGateway, APIInsufficientPermissions, + APIOrganizationNotAllowed): + # Let all our custom exceptions propagate up unchanged + raise except Exception as error: - response = Response(text=f"{error}", error=True, status_code=500) - raise APIFailure(response) + # Only truly unexpected errors get wrapped in a generic APIFailure + raise APIFailure(f"Unexpected error: {error}", status_code=500) diff --git a/socketdev/exceptions.py b/socketdev/exceptions.py index 735aec2..d61b827 100644 --- a/socketdev/exceptions.py +++ b/socketdev/exceptions.py @@ -1,22 +1,47 @@ -class APIKeyMissing(Exception): +class APIFailure(Exception): + """Base exception for all Socket API errors""" + pass + + +class APIKeyMissing(APIFailure): """Raised when the api key is not passed and the headers are empty""" -class APIFailure(Exception): - """Raised when there is an error using the API""" +class APIAccessDenied(APIFailure): + """Raised when access is denied to the API""" pass -class APIAccessDenied(Exception): - """Raised when access is denied to the API""" +class APIInsufficientPermissions(APIFailure): + """Raised when the API token doesn't have required permissions""" pass -class APIInsufficientQuota(Exception): - """Raised when access is denied to the API""" +class APIOrganizationNotAllowed(APIFailure): + """Raised when organization doesn't have access to the feature""" pass -class APIResourceNotFound(Exception): - """Raised when access is denied to the API""" +class APIInsufficientQuota(APIFailure): + """Raised when access is denied to the API due to quota limits""" + pass + + +class APIResourceNotFound(APIFailure): + """Raised when the requested resource is not found""" + pass + + +class APITimeout(APIFailure): + """Raised when a request times out""" pass + + +class APIConnectionError(APIFailure): + """Raised when there's a connection error""" + pass + + +class APIBadGateway(APIFailure): + """Raised when the upstream server returns a 502 Bad Gateway error""" + pass \ No newline at end of file diff --git a/socketdev/version.py b/socketdev/version.py index f30c4f0..6ff6d62 100644 --- a/socketdev/version.py +++ b/socketdev/version.py @@ -1 +1 @@ -__version__ = "2.0.6" \ No newline at end of file +__version__ = "2.0.7" \ No newline at end of file