Skip to content

Commit 0e03f4e

Browse files
committed
fix: stop invalidating token on rate limit code 4, add rate limit header logging
1 parent 1b06a2e commit 0e03f4e

File tree

1 file changed

+71
-17
lines changed

1 file changed

+71
-17
lines changed

meta_ads_mcp/core/api.py

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,49 @@ def __init__(self, error_data: Dict[str, Any]):
3131
logger.error(f"Graph API Error: {self.message}")
3232
logger.debug(f"Error details: {error_data}")
3333

34-
# Check if this is an auth error
35-
if "code" in error_data and error_data["code"] in [190, 102, 4]:
36-
# Common auth error codes
34+
# Check if this is an auth error (code 4 is rate limiting, NOT auth)
35+
if "code" in error_data and error_data["code"] in [190, 102]:
3736
logger.warning(f"Auth error detected (code: {error_data['code']}). Invalidating token.")
3837
auth_manager.invalidate_token()
38+
elif "code" in error_data and error_data["code"] == 4:
39+
logger.warning(f"Rate limit error detected (code: 4, subcode: {error_data.get('error_subcode', 'N/A')}). Token is still valid — NOT invalidating.")
40+
41+
42+
def _log_meta_rate_limit_headers(headers: dict, endpoint: str) -> None:
43+
"""Log Meta's rate limit headers for observability (X-App-Usage, X-Business-Use-Case-Usage)."""
44+
app_usage = headers.get("x-app-usage")
45+
biz_usage = headers.get("x-business-use-case-usage")
46+
ad_account_usage = headers.get("x-ad-account-usage")
47+
48+
if app_usage or biz_usage or ad_account_usage:
49+
usage_data = {}
50+
if app_usage:
51+
try:
52+
usage_data["app_usage"] = json.loads(app_usage)
53+
except (json.JSONDecodeError, TypeError):
54+
usage_data["app_usage_raw"] = str(app_usage)
55+
if biz_usage:
56+
try:
57+
usage_data["business_use_case_usage"] = json.loads(biz_usage)
58+
except (json.JSONDecodeError, TypeError):
59+
usage_data["business_use_case_usage_raw"] = str(biz_usage)
60+
if ad_account_usage:
61+
try:
62+
usage_data["ad_account_usage"] = json.loads(ad_account_usage)
63+
except (json.JSONDecodeError, TypeError):
64+
usage_data["ad_account_usage_raw"] = str(ad_account_usage)
65+
66+
# Warn at high usage levels (any field >= 80%)
67+
is_high = False
68+
for key, val in usage_data.items():
69+
if isinstance(val, dict):
70+
for metric, pct in val.items():
71+
if isinstance(pct, (int, float)) and pct >= 80:
72+
is_high = True
73+
break
74+
75+
log_fn = logger.warning if is_high else logger.info
76+
log_fn(f"meta_rate_limit_usage endpoint={endpoint} {json.dumps(usage_data)}")
3977

4078

4179
async def make_api_request(
@@ -116,7 +154,10 @@ async def make_api_request(
116154

117155
response.raise_for_status()
118156
logger.debug(f"API Response status: {response.status_code}")
119-
157+
158+
# Log Meta rate limit headers for observability
159+
_log_meta_rate_limit_headers(response.headers, endpoint)
160+
120161
# Ensure the response is JSON and return it as a dictionary
121162
try:
122163
return response.json()
@@ -135,29 +176,42 @@ async def make_api_request(
135176
error_info = {"status_code": e.response.status_code, "text": e.response.text}
136177

137178
logger.error(f"HTTP Error: {e.response.status_code} - {error_info}")
138-
139-
# Check for authentication errors
140-
if e.response.status_code == 401 or e.response.status_code == 403:
141-
logger.warning("Detected authentication error (401/403)")
142-
auth_manager.invalidate_token()
143-
elif "error" in error_info:
179+
180+
# Log Meta rate limit headers even on errors
181+
_log_meta_rate_limit_headers(e.response.headers, endpoint)
182+
183+
# Check for rate limit errors vs authentication errors.
184+
# Code 4 is a rate limit (NOT auth) — do NOT invalidate token.
185+
if "error" in error_info:
144186
error_obj = error_info.get("error", {})
145-
# Check for specific FB API errors related to auth
146-
if isinstance(error_obj, dict) and error_obj.get("code") in [190, 102, 4, 200, 10]:
147-
logger.warning(f"Detected Facebook API auth error: {error_obj.get('code')}")
148-
# Log more details about app ID related errors
149-
if error_obj.get("code") == 200 and "Provide valid app ID" in error_obj.get("message", ""):
187+
error_code = error_obj.get("code") if isinstance(error_obj, dict) else None
188+
189+
if error_code == 4:
190+
# Application-level rate limit — token is still valid
191+
logger.warning(
192+
f"Facebook API rate limit (code=4, subcode={error_obj.get('error_subcode', 'N/A')}, "
193+
f"msg={error_obj.get('error_user_msg', error_obj.get('message', 'N/A'))}). "
194+
f"Token is still valid — NOT invalidating."
195+
)
196+
elif error_code in [190, 102, 200, 10]:
197+
logger.warning(f"Detected Facebook API auth error: {error_code}")
198+
if error_code == 200 and "Provide valid app ID" in error_obj.get("message", ""):
150199
logger.error("Meta API authentication configuration issue")
151200
logger.error(f"Current app_id: {app_id}")
152-
# Provide a clearer error message without the confusing "Provide valid app ID" message
153201
return {
154202
"error": {
155203
"message": "Meta API authentication configuration issue. Please check your app credentials.",
156204
"original_error": error_obj.get("message"),
157-
"code": error_obj.get("code")
205+
"code": error_code
158206
}
159207
}
160208
auth_manager.invalidate_token()
209+
elif e.response.status_code in [401, 403]:
210+
logger.warning(f"Detected authentication error ({e.response.status_code})")
211+
auth_manager.invalidate_token()
212+
elif e.response.status_code in [401, 403]:
213+
logger.warning(f"Detected authentication error ({e.response.status_code})")
214+
auth_manager.invalidate_token()
161215

162216
# Include full details for technical users
163217
full_response = {

0 commit comments

Comments
 (0)