Skip to content

improved error handling #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions socketdev/core/api.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
43 changes: 34 additions & 9 deletions socketdev/exceptions.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion socketdev/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.0.6"
__version__ = "2.0.7"
Loading