@@ -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
4179async 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