Skip to content

Commit 12e1bd5

Browse files
authored
improved error handling (#22)
1 parent 4ffc7d6 commit 12e1bd5

File tree

3 files changed

+85
-16
lines changed

3 files changed

+85
-16
lines changed

socketdev/core/api.py

+50-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import base64
22
import requests
33
from socketdev.core.classes import Response
4-
from socketdev.exceptions import APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota, APIResourceNotFound
4+
from socketdev.exceptions import (
5+
APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota,
6+
APIResourceNotFound, APITimeout, APIConnectionError, APIBadGateway,
7+
APIInsufficientPermissions, APIOrganizationNotAllowed
8+
)
59
from socketdev.version import __version__
10+
from requests.exceptions import Timeout, ConnectionError
11+
import time
612

713

814
class API:
@@ -31,23 +37,61 @@ def do_request(
3137
}
3238
url = f"{self.api_url}/{path}"
3339
try:
40+
start_time = time.time()
3441
response = requests.request(
3542
method.upper(), url, headers=headers, data=payload, files=files, timeout=self.request_timeout
3643
)
44+
request_duration = time.time() - start_time
3745

3846
if response.status_code == 401:
3947
raise APIAccessDenied("Unauthorized")
4048
if response.status_code == 403:
41-
raise APIInsufficientQuota("Insufficient max_quota for API method")
49+
try:
50+
error_message = response.json().get('error', {}).get('message', '')
51+
if "Insufficient permissions for API method" in error_message:
52+
raise APIInsufficientPermissions(error_message)
53+
elif "Organization not allowed" in error_message:
54+
raise APIOrganizationNotAllowed(error_message)
55+
elif "Insufficient max quota" in error_message:
56+
raise APIInsufficientQuota(error_message)
57+
else:
58+
raise APIAccessDenied(error_message or "Access denied")
59+
except ValueError:
60+
# If JSON parsing fails
61+
raise APIAccessDenied("Access denied")
4262
if response.status_code == 404:
4363
raise APIResourceNotFound(f"Path not found {path}")
4464
if response.status_code == 429:
45-
raise APIInsufficientQuota("Insufficient quota for API route")
65+
retry_after = response.headers.get('retry-after')
66+
if retry_after:
67+
try:
68+
seconds = int(retry_after)
69+
minutes = seconds // 60
70+
remaining_seconds = seconds % 60
71+
time_msg = f" Quota will reset in {minutes} minutes and {remaining_seconds} seconds"
72+
except ValueError:
73+
time_msg = f" Retry after: {retry_after}"
74+
else:
75+
time_msg = ""
76+
raise APIInsufficientQuota(f"Insufficient quota for API route.{time_msg}")
77+
if response.status_code == 502:
78+
raise APIBadGateway("Upstream server error")
4679
if response.status_code >= 400:
47-
raise APIFailure("Bad Request")
80+
raise APIFailure(f"Bad Request: HTTP {response.status_code}")
4881

4982
return response
5083

84+
except Timeout:
85+
request_duration = time.time() - start_time
86+
raise APITimeout(f"Request timed out after {request_duration:.2f} seconds")
87+
except ConnectionError as error:
88+
request_duration = time.time() - start_time
89+
raise APIConnectionError(f"Connection error after {request_duration:.2f} seconds: {error}")
90+
except (APIAccessDenied, APIInsufficientQuota, APIResourceNotFound, APIFailure,
91+
APITimeout, APIConnectionError, APIBadGateway, APIInsufficientPermissions,
92+
APIOrganizationNotAllowed):
93+
# Let all our custom exceptions propagate up unchanged
94+
raise
5195
except Exception as error:
52-
response = Response(text=f"{error}", error=True, status_code=500)
53-
raise APIFailure(response)
96+
# Only truly unexpected errors get wrapped in a generic APIFailure
97+
raise APIFailure(f"Unexpected error: {error}", status_code=500)

socketdev/exceptions.py

+34-9
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,47 @@
1-
class APIKeyMissing(Exception):
1+
class APIFailure(Exception):
2+
"""Base exception for all Socket API errors"""
3+
pass
4+
5+
6+
class APIKeyMissing(APIFailure):
27
"""Raised when the api key is not passed and the headers are empty"""
38

49

5-
class APIFailure(Exception):
6-
"""Raised when there is an error using the API"""
10+
class APIAccessDenied(APIFailure):
11+
"""Raised when access is denied to the API"""
712
pass
813

914

10-
class APIAccessDenied(Exception):
11-
"""Raised when access is denied to the API"""
15+
class APIInsufficientPermissions(APIFailure):
16+
"""Raised when the API token doesn't have required permissions"""
1217
pass
1318

1419

15-
class APIInsufficientQuota(Exception):
16-
"""Raised when access is denied to the API"""
20+
class APIOrganizationNotAllowed(APIFailure):
21+
"""Raised when organization doesn't have access to the feature"""
1722
pass
1823

1924

20-
class APIResourceNotFound(Exception):
21-
"""Raised when access is denied to the API"""
25+
class APIInsufficientQuota(APIFailure):
26+
"""Raised when access is denied to the API due to quota limits"""
27+
pass
28+
29+
30+
class APIResourceNotFound(APIFailure):
31+
"""Raised when the requested resource is not found"""
32+
pass
33+
34+
35+
class APITimeout(APIFailure):
36+
"""Raised when a request times out"""
2237
pass
38+
39+
40+
class APIConnectionError(APIFailure):
41+
"""Raised when there's a connection error"""
42+
pass
43+
44+
45+
class APIBadGateway(APIFailure):
46+
"""Raised when the upstream server returns a 502 Bad Gateway error"""
47+
pass

socketdev/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "2.0.6"
1+
__version__ = "2.0.7"

0 commit comments

Comments
 (0)