jebin2 commited on
Commit
df73137
·
1 Parent(s): a569fc1

fix(network): Implement DoH-based DNS patch and robust Session handling for Social Publishers

Browse files
social_media_publishers/base.py CHANGED
@@ -45,6 +45,11 @@ class SocialAuthCreator(ABC):
45
 
46
  # --- Common Implementation for Token Management ---
47
 
 
 
 
 
 
48
  def _get_token_manager(self):
49
  """Initialize TokenManager from environment variables."""
50
  encryption_key = get_config_value("encryption_key")
@@ -175,6 +180,11 @@ class SocialAuthCreator(ABC):
175
  class SocialPublisher(ABC):
176
  """Abstract base class for publishing content to social media."""
177
 
 
 
 
 
 
178
  @abstractmethod
179
  def authenticate(self, account_id: Optional[str] = None) -> Any:
180
  """
 
45
 
46
  # --- Common Implementation for Token Management ---
47
 
48
+ def _get_session(self):
49
+ """Get a requests Session with resilient network configuration (retries, custom DNS)."""
50
+ from src.network_utils import get_resilient_session
51
+ return get_resilient_session()
52
+
53
  def _get_token_manager(self):
54
  """Initialize TokenManager from environment variables."""
55
  encryption_key = get_config_value("encryption_key")
 
180
  class SocialPublisher(ABC):
181
  """Abstract base class for publishing content to social media."""
182
 
183
+ def _get_session(self):
184
+ """Get a requests Session with resilient network configuration (retries, custom DNS)."""
185
+ from src.network_utils import get_resilient_session
186
+ return get_resilient_session()
187
+
188
  @abstractmethod
189
  def authenticate(self, account_id: Optional[str] = None) -> Any:
190
  """
social_media_publishers/facebook/auth.py CHANGED
@@ -101,7 +101,7 @@ class FacebookAuthCreator(SocialAuthCreator):
101
  'code': code
102
  }
103
 
104
- response = requests.get(self.TOKEN_URL, params=token_params)
105
  if response.status_code != 200:
106
  return {"success": False, "error": f"Token exchange failed: {response.text}"}
107
 
@@ -116,7 +116,7 @@ class FacebookAuthCreator(SocialAuthCreator):
116
  'fb_exchange_token': short_lived_token
117
  }
118
 
119
- ll_response = requests.get(self.TOKEN_URL, params=long_lived_params)
120
  if ll_response.status_code == 200:
121
  ll_data = ll_response.json()
122
  user_access_token = ll_data.get('access_token', short_lived_token)
@@ -126,7 +126,7 @@ class FacebookAuthCreator(SocialAuthCreator):
126
  print("⚠️ Could not get long-lived token, using short-lived")
127
 
128
  # Get user's Facebook Pages
129
- pages_response = requests.get(
130
  f"{self.GRAPH_URL}/me/accounts",
131
  params={'access_token': user_access_token}
132
  )
@@ -166,7 +166,7 @@ class FacebookAuthCreator(SocialAuthCreator):
166
  }
167
 
168
  try:
169
- page_ll_response = requests.get(self.TOKEN_URL, params=page_ll_params)
170
  if page_ll_response.status_code == 200:
171
  page_ll_data = page_ll_response.json()
172
  page_access_token = page_ll_data.get('access_token', page_access_token)
 
101
  'code': code
102
  }
103
 
104
+ response = self._get_session().get(self.TOKEN_URL, params=token_params)
105
  if response.status_code != 200:
106
  return {"success": False, "error": f"Token exchange failed: {response.text}"}
107
 
 
116
  'fb_exchange_token': short_lived_token
117
  }
118
 
119
+ ll_response = self._get_session().get(self.TOKEN_URL, params=long_lived_params)
120
  if ll_response.status_code == 200:
121
  ll_data = ll_response.json()
122
  user_access_token = ll_data.get('access_token', short_lived_token)
 
126
  print("⚠️ Could not get long-lived token, using short-lived")
127
 
128
  # Get user's Facebook Pages
129
+ pages_response = self._get_session().get(
130
  f"{self.GRAPH_URL}/me/accounts",
131
  params={'access_token': user_access_token}
132
  )
 
166
  }
167
 
168
  try:
169
+ page_ll_response = self._get_session().get(self.TOKEN_URL, params=page_ll_params)
170
  if page_ll_response.status_code == 200:
171
  page_ll_data = page_ll_response.json()
172
  page_access_token = page_ll_data.get('access_token', page_access_token)
social_media_publishers/facebook/publisher.py CHANGED
@@ -141,7 +141,7 @@ class FacebookPublisher(SocialPublisher):
141
 
142
  # Step 1: Start upload session
143
  print(f"🎬 Starting Facebook video upload...")
144
- start_response = requests.post(
145
  f"{self.GRAPH_URL}/{self.page_id}/videos",
146
  params={
147
  'access_token': self.access_token,
@@ -174,7 +174,7 @@ class FacebookPublisher(SocialPublisher):
174
  progress = int((end_offset / file_size) * 100)
175
  print(f"📤 Uploading: {progress}%")
176
 
177
- transfer_response = requests.post(
178
  f"{self.GRAPH_URL}/{self.page_id}/videos",
179
  params={
180
  'access_token': self.access_token,
@@ -193,7 +193,7 @@ class FacebookPublisher(SocialPublisher):
193
 
194
  # Step 3: Finish upload
195
  print("🚀 Finishing upload...")
196
- finish_response = requests.post(
197
  f"{self.GRAPH_URL}/{self.page_id}/videos",
198
  params={
199
  'access_token': self.access_token,
@@ -244,7 +244,7 @@ class FacebookPublisher(SocialPublisher):
244
  params['after'] = page_token
245
 
246
  print(f"DEBUG: Fetching videos from {endpoint} with params: {params}")
247
- response = requests.get(endpoint, params=params)
248
 
249
  p_text = response.text
250
  print(f"DEBUG: Video Fetch Response: {p_text}")
@@ -263,7 +263,7 @@ class FacebookPublisher(SocialPublisher):
263
 
264
  # Probe 2: Try without 'type=uploaded'
265
  try:
266
- all_vid_resp = requests.get(f"{self.GRAPH_URL}/{self.page_id}/videos", params={'access_token': self.access_token, 'limit': 3})
267
  print(f"DEBUG: Probe /videos (no type): {all_vid_resp.text}")
268
  except: pass
269
 
@@ -342,7 +342,7 @@ class FacebookPublisher(SocialPublisher):
342
  if video['views'] > 0: continue # Skip if we already have views from fields
343
 
344
  try:
345
- insights_response = requests.get(
346
  f"{self.GRAPH_URL}/{video['id']}/video_insights",
347
  params={
348
  'access_token': self.access_token,
@@ -400,7 +400,7 @@ class FacebookPublisher(SocialPublisher):
400
 
401
  # Try fetching Page/User info
402
  try:
403
- response = requests.get(endpoint, params=params)
404
  if response.status_code == 200:
405
  page_data = response.json()
406
  page_name = page_data.get('name', 'Unknown')
@@ -414,7 +414,7 @@ class FacebookPublisher(SocialPublisher):
414
  print(f"DEBUG: Stats - Page Info Fetch failed ({response.status_code}): {response.text}")
415
  # Fallback for User Profile (simple fields)
416
  params['fields'] = 'name,picture'
417
- response = requests.get(endpoint, params=params)
418
  if response.status_code == 200:
419
  page_data = response.json()
420
  page_name = page_data.get('name', 'Unknown')
 
141
 
142
  # Step 1: Start upload session
143
  print(f"🎬 Starting Facebook video upload...")
144
+ start_response = self._get_session().post(
145
  f"{self.GRAPH_URL}/{self.page_id}/videos",
146
  params={
147
  'access_token': self.access_token,
 
174
  progress = int((end_offset / file_size) * 100)
175
  print(f"📤 Uploading: {progress}%")
176
 
177
+ transfer_response = self._get_session().post(
178
  f"{self.GRAPH_URL}/{self.page_id}/videos",
179
  params={
180
  'access_token': self.access_token,
 
193
 
194
  # Step 3: Finish upload
195
  print("🚀 Finishing upload...")
196
+ finish_response = self._get_session().post(
197
  f"{self.GRAPH_URL}/{self.page_id}/videos",
198
  params={
199
  'access_token': self.access_token,
 
244
  params['after'] = page_token
245
 
246
  print(f"DEBUG: Fetching videos from {endpoint} with params: {params}")
247
+ response = self._get_session().get(endpoint, params=params)
248
 
249
  p_text = response.text
250
  print(f"DEBUG: Video Fetch Response: {p_text}")
 
263
 
264
  # Probe 2: Try without 'type=uploaded'
265
  try:
266
+ all_vid_resp = self._get_session().get(f"{self.GRAPH_URL}/{self.page_id}/videos", params={'access_token': self.access_token, 'limit': 3})
267
  print(f"DEBUG: Probe /videos (no type): {all_vid_resp.text}")
268
  except: pass
269
 
 
342
  if video['views'] > 0: continue # Skip if we already have views from fields
343
 
344
  try:
345
+ insights_response = self._get_session().get(
346
  f"{self.GRAPH_URL}/{video['id']}/video_insights",
347
  params={
348
  'access_token': self.access_token,
 
400
 
401
  # Try fetching Page/User info
402
  try:
403
+ response = self._get_session().get(endpoint, params=params)
404
  if response.status_code == 200:
405
  page_data = response.json()
406
  page_name = page_data.get('name', 'Unknown')
 
414
  print(f"DEBUG: Stats - Page Info Fetch failed ({response.status_code}): {response.text}")
415
  # Fallback for User Profile (simple fields)
416
  params['fields'] = 'name,picture'
417
+ response = self._get_session().get(endpoint, params=params)
418
  if response.status_code == 200:
419
  page_data = response.json()
420
  page_name = page_data.get('name', 'Unknown')
social_media_publishers/instagram/auth.py CHANGED
@@ -101,7 +101,7 @@ class InstagramAuthCreator(SocialAuthCreator):
101
  'code': code
102
  }
103
 
104
- response = requests.get(self.TOKEN_URL, params=token_params)
105
  if response.status_code != 200:
106
  return {"success": False, "error": f"Token exchange failed: {response.text}"}
107
 
@@ -116,7 +116,7 @@ class InstagramAuthCreator(SocialAuthCreator):
116
  'fb_exchange_token': short_lived_token
117
  }
118
 
119
- ll_response = requests.get(self.TOKEN_URL, params=long_lived_params)
120
  if ll_response.status_code == 200:
121
  ll_data = ll_response.json()
122
  access_token = ll_data.get('access_token', short_lived_token)
@@ -126,7 +126,7 @@ class InstagramAuthCreator(SocialAuthCreator):
126
  print("⚠️ Could not get long-lived token, using short-lived")
127
 
128
  # Get Facebook Pages linked to account
129
- pages_response = requests.get(
130
  f"{self.GRAPH_URL}/me/accounts",
131
  params={'access_token': access_token}
132
  )
@@ -160,7 +160,7 @@ class InstagramAuthCreator(SocialAuthCreator):
160
 
161
  # Check for linked Instagram account
162
  try:
163
- ig_response = requests.get(
164
  f"{self.GRAPH_URL}/{page_id}",
165
  params={
166
  'fields': 'instagram_business_account',
@@ -185,7 +185,7 @@ class InstagramAuthCreator(SocialAuthCreator):
185
  ig_account_id = ig_account.get('id')
186
 
187
  # Get Instagram username
188
- ig_info_response = requests.get(
189
  f"{self.GRAPH_URL}/{ig_account_id}",
190
  params={
191
  'fields': 'username',
 
101
  'code': code
102
  }
103
 
104
+ response = self._get_session().get(self.TOKEN_URL, params=token_params)
105
  if response.status_code != 200:
106
  return {"success": False, "error": f"Token exchange failed: {response.text}"}
107
 
 
116
  'fb_exchange_token': short_lived_token
117
  }
118
 
119
+ ll_response = self._get_session().get(self.TOKEN_URL, params=long_lived_params)
120
  if ll_response.status_code == 200:
121
  ll_data = ll_response.json()
122
  access_token = ll_data.get('access_token', short_lived_token)
 
126
  print("⚠️ Could not get long-lived token, using short-lived")
127
 
128
  # Get Facebook Pages linked to account
129
+ pages_response = self._get_session().get(
130
  f"{self.GRAPH_URL}/me/accounts",
131
  params={'access_token': access_token}
132
  )
 
160
 
161
  # Check for linked Instagram account
162
  try:
163
+ ig_response = self._get_session().get(
164
  f"{self.GRAPH_URL}/{page_id}",
165
  params={
166
  'fields': 'instagram_business_account',
 
185
  ig_account_id = ig_account.get('id')
186
 
187
  # Get Instagram username
188
+ ig_info_response = self._get_session().get(
189
  f"{self.GRAPH_URL}/{ig_account_id}",
190
  params={
191
  'fields': 'username',
social_media_publishers/instagram/publisher.py CHANGED
@@ -206,7 +206,7 @@ class InstagramPublisher(SocialPublisher):
206
  """POST with retry logic."""
207
  for attempt in range(1, max_retries + 1):
208
  try:
209
- r = requests.post(url, params=params, timeout=30)
210
  if r.status_code == 200:
211
  return r
212
  print(f"⚠️ Attempt {attempt} failed ({r.status_code}): {r.text}")
@@ -222,7 +222,7 @@ class InstagramPublisher(SocialPublisher):
222
 
223
  for attempt in range(1, max_attempts + 1):
224
  params = {'access_token': self.access_token, 'fields': 'status_code'}
225
- response = requests.get(endpoint, params=params)
226
  result = response.json()
227
 
228
  status = result.get('status_code')
@@ -281,7 +281,7 @@ class InstagramPublisher(SocialPublisher):
281
  if page_token:
282
  params['after'] = page_token
283
 
284
- response = requests.get(endpoint, params=params)
285
  if response.status_code != 200:
286
  return {'error': f"API error: {response.text}", 'videos': []}
287
 
@@ -319,7 +319,7 @@ class InstagramPublisher(SocialPublisher):
319
  if media_type in ('VIDEO', 'REELS'):
320
  # Fetch insights for this media
321
  try:
322
- insights_resp = requests.get(
323
  f"{self.GRAPH_URL}/{media_id}/insights",
324
  params={
325
  'access_token': self.access_token,
@@ -383,7 +383,7 @@ class InstagramPublisher(SocialPublisher):
383
  'fields': 'username,profile_picture_url,followers_count,media_count'
384
  }
385
 
386
- response = requests.get(endpoint, params=params)
387
  if response.status_code != 200:
388
  return {'error': f"API error: {response.text}"}
389
 
 
206
  """POST with retry logic."""
207
  for attempt in range(1, max_retries + 1):
208
  try:
209
+ r = self._get_session().post(url, params=params, timeout=30)
210
  if r.status_code == 200:
211
  return r
212
  print(f"⚠️ Attempt {attempt} failed ({r.status_code}): {r.text}")
 
222
 
223
  for attempt in range(1, max_attempts + 1):
224
  params = {'access_token': self.access_token, 'fields': 'status_code'}
225
+ response = self._get_session().get(endpoint, params=params)
226
  result = response.json()
227
 
228
  status = result.get('status_code')
 
281
  if page_token:
282
  params['after'] = page_token
283
 
284
+ response = self._get_session().get(endpoint, params=params)
285
  if response.status_code != 200:
286
  return {'error': f"API error: {response.text}", 'videos': []}
287
 
 
319
  if media_type in ('VIDEO', 'REELS'):
320
  # Fetch insights for this media
321
  try:
322
+ insights_resp = self._get_session().get(
323
  f"{self.GRAPH_URL}/{media_id}/insights",
324
  params={
325
  'access_token': self.access_token,
 
383
  'fields': 'username,profile_picture_url,followers_count,media_count'
384
  }
385
 
386
+ response = self._get_session().get(endpoint, params=params)
387
  if response.status_code != 200:
388
  return {'error': f"API error: {response.text}"}
389
 
social_media_publishers/threads/auth.py CHANGED
@@ -110,7 +110,7 @@ class ThreadsAuthCreator(SocialAuthCreator):
110
  print(f"DEBUG: Threads Token Exchange Params: client_id={config['app_id'][:5]}..., redirect_uri={redirect_uri}")
111
 
112
  # Threads token exchange
113
- response = requests.post(self.TOKEN_URL, data=token_data_body, params=token_url_params)
114
  if response.status_code != 200:
115
  short_token_params = token_params.copy()
116
  # Retry with GET or check docs? Threads API uses POST.
@@ -133,7 +133,7 @@ class ThreadsAuthCreator(SocialAuthCreator):
133
  'access_token': short_lived_token
134
  }
135
 
136
- ll_response = requests.get(long_lived_url, params=long_lived_params)
137
 
138
  if ll_response.status_code == 200:
139
  ll_data = ll_response.json()
@@ -145,7 +145,7 @@ class ThreadsAuthCreator(SocialAuthCreator):
145
 
146
  # Get User Profile Info
147
  # Endpoint: /me?fields=id,username,name,threads_profile_picture_url
148
- profile_response = requests.get(
149
  f"{self.GRAPH_URL}/me",
150
  params={
151
  'fields': 'id,username,name,threads_profile_picture_url',
 
110
  print(f"DEBUG: Threads Token Exchange Params: client_id={config['app_id'][:5]}..., redirect_uri={redirect_uri}")
111
 
112
  # Threads token exchange
113
+ response = self._get_session().post(self.TOKEN_URL, data=token_data_body, params=token_url_params)
114
  if response.status_code != 200:
115
  short_token_params = token_params.copy()
116
  # Retry with GET or check docs? Threads API uses POST.
 
133
  'access_token': short_lived_token
134
  }
135
 
136
+ ll_response = self._get_session().get(long_lived_url, params=long_lived_params)
137
 
138
  if ll_response.status_code == 200:
139
  ll_data = ll_response.json()
 
145
 
146
  # Get User Profile Info
147
  # Endpoint: /me?fields=id,username,name,threads_profile_picture_url
148
+ profile_response = self._get_session().get(
149
  f"{self.GRAPH_URL}/me",
150
  params={
151
  'fields': 'id,username,name,threads_profile_picture_url',
social_media_publishers/threads/publisher.py CHANGED
@@ -226,7 +226,7 @@ class ThreadsPublisher(SocialPublisher):
226
  """POST with retry logic."""
227
  for attempt in range(1, max_retries + 1):
228
  try:
229
- r = requests.post(url, params=params, timeout=30)
230
  if r.status_code == 200:
231
  return r
232
  print(f"⚠️ Attempt {attempt} failed ({r.status_code}): {r.text}")
@@ -242,7 +242,7 @@ class ThreadsPublisher(SocialPublisher):
242
 
243
  for attempt in range(1, max_attempts + 1):
244
  params = {'access_token': self.access_token, 'fields': 'status'}
245
- response = requests.get(endpoint, params=params)
246
  result = response.json()
247
 
248
  status = result.get('status')
@@ -271,7 +271,7 @@ class ThreadsPublisher(SocialPublisher):
271
  'access_token': self.access_token,
272
  'fields': 'id,username,name,threads_profile_picture_url,followers_count' # followers_count might not be available on all versions, but let's try
273
  }
274
- me_resp = requests.get(me_endpoint, params=me_params)
275
  me_data = me_resp.json()
276
 
277
  username = me_data.get('username', 'unknown')
@@ -294,7 +294,7 @@ class ThreadsPublisher(SocialPublisher):
294
  'limit': 50
295
  }
296
 
297
- media_resp = requests.get(media_endpoint, params=media_params)
298
  media_data = media_resp.json().get('data', [])
299
 
300
  total_likes = 0
@@ -349,7 +349,7 @@ class ThreadsPublisher(SocialPublisher):
349
  if page_token:
350
  params['after'] = page_token
351
 
352
- response = requests.get(endpoint, params=params)
353
  data = response.json()
354
 
355
  if 'error' in data:
 
226
  """POST with retry logic."""
227
  for attempt in range(1, max_retries + 1):
228
  try:
229
+ r = self._get_session().post(url, params=params, timeout=30)
230
  if r.status_code == 200:
231
  return r
232
  print(f"⚠️ Attempt {attempt} failed ({r.status_code}): {r.text}")
 
242
 
243
  for attempt in range(1, max_attempts + 1):
244
  params = {'access_token': self.access_token, 'fields': 'status'}
245
+ response = self._get_session().get(endpoint, params=params)
246
  result = response.json()
247
 
248
  status = result.get('status')
 
271
  'access_token': self.access_token,
272
  'fields': 'id,username,name,threads_profile_picture_url,followers_count' # followers_count might not be available on all versions, but let's try
273
  }
274
+ me_resp = self._get_session().get(me_endpoint, params=me_params)
275
  me_data = me_resp.json()
276
 
277
  username = me_data.get('username', 'unknown')
 
294
  'limit': 50
295
  }
296
 
297
+ media_resp = self._get_session().get(media_endpoint, params=media_params)
298
  media_data = media_resp.json().get('data', [])
299
 
300
  total_likes = 0
 
349
  if page_token:
350
  params['after'] = page_token
351
 
352
+ response = self._get_session().get(endpoint, params=params)
353
  data = response.json()
354
 
355
  if 'error' in data:
src/network_utils.py CHANGED
@@ -1,39 +1,85 @@
1
- import socket
2
  import logging
 
 
 
 
 
3
 
4
  logger = logging.getLogger(__name__)
5
 
6
- def allowed_gai_family():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  """
8
- https://github.com/shazow/urllib3/blob/master/urllib3/util/connection.py
9
  """
10
- family = socket.AF_INET
11
- return family
 
 
 
 
 
 
 
 
 
12
 
13
  def force_ipv4_requests():
14
  """
15
- Monkey-patch urllib3 to force IPv4.
16
- Useful for containerized environments with broken IPv6 DNS.
 
 
 
 
 
 
 
 
 
 
17
  """
18
- patched = False
 
 
 
 
19
 
20
- # Try patching standalone urllib3
21
- try:
22
- import urllib3.util.connection as urllib3_cn
23
- urllib3_cn.allowed_gai_family = allowed_gai_family
24
- patched = True
25
- logger.info("✅ Patched urllib3 for IPv4-only requests")
26
- except ImportError:
27
- pass
28
 
29
- # Try patching requests vendored urllib3
30
- try:
31
- import requests.packages.urllib3.util.connection as requests_urllib3_cn
32
- requests_urllib3_cn.allowed_gai_family = allowed_gai_family
33
- patched = True
34
- logger.info("✅ Patched requests.packages.urllib3 for IPv4-only requests")
35
- except ImportError:
36
- pass
37
-
38
- if not patched:
39
- logger.warning("⚠️ Could not patch urllib3 for IPv4 (modules not found)")
 
 
1
  import logging
2
+ import socket
3
+ import json
4
+ import requests
5
+ from requests.adapters import HTTPAdapter
6
+ from requests.packages.urllib3.util.retry import Retry
7
 
8
  logger = logging.getLogger(__name__)
9
 
10
+ # Cache for resolved IPs to avoid spamming DoH
11
+ _DNS_CACHE = {}
12
+ _ORIGINAL_GETADDRINFO = socket.getaddrinfo
13
+
14
+ def resolve_via_doh(hostname):
15
+ """
16
+ Resolve hostname using Google DNS-over-HTTPS.
17
+ Bypasses local system resolver (/etc/resolv.conf).
18
+ """
19
+ if hostname in _DNS_CACHE:
20
+ return _DNS_CACHE[hostname]
21
+
22
+ try:
23
+ # We must use an IP for the DoH server to avoid DNS loop!
24
+ # 8.8.8.8 is Google DNS.
25
+ doh_url = f"https://8.8.8.8/resolve?name={hostname}&type=A"
26
+
27
+ # Use a fresh simple session without adapters to avoid recursion
28
+ # But wait, requests might try to resolve 8.8.8.8? No, it's an IP.
29
+ resp = requests.get(doh_url, timeout=5, verify=False) # Skip verify for DoH to be fast/safe
30
+ data = resp.json()
31
+
32
+ if 'Answer' in data:
33
+ for answer in data['Answer']:
34
+ if answer['type'] == 1: # A Record
35
+ ip = answer['data']
36
+ logger.info(f"🔍 DoH Resolved {hostname} -> {ip}")
37
+ _DNS_CACHE[hostname] = ip
38
+ return ip
39
+ except Exception as e:
40
+ logger.error(f"⚠️ DoH Resolution failed for {hostname}: {e}")
41
+
42
+ return None
43
+
44
+ def patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
45
  """
46
+ Custom getaddrinfo that attempts DoH if system resolution fails or for specific hosts.
47
  """
48
+ # Only intervene for Facebook graph API which is failing
49
+ if host == 'graph.facebook.com' or host == 'www.facebook.com':
50
+ ip = resolve_via_doh(host)
51
+ if ip:
52
+ # Call original with IP instead of hostname
53
+ # Log usage for debugging
54
+ # logger.debug(f"Redirecting {host} to {ip}")
55
+ return _ORIGINAL_GETADDRINFO(ip, port, family, type, proto, flags)
56
+
57
+ # Fallback to system resolver
58
+ return _ORIGINAL_GETADDRINFO(host, port, family, type, proto, flags)
59
 
60
  def force_ipv4_requests():
61
  """
62
+ Installs the DNS patch.
63
+ Named 'force_ipv4_requests' for backward compatibility with existing calls.
64
+ """
65
+ if socket.getaddrinfo != patched_getaddrinfo:
66
+ socket.getaddrinfo = patched_getaddrinfo
67
+ logger.info("🛡️ Installed DoH-based DNS patch for Facebook API")
68
+ else:
69
+ logger.info("🛡️ DNS patch already active")
70
+
71
+ def get_resilient_session():
72
+ """
73
+ Returns a session with retry logic.
74
  """
75
+ session = requests.Session()
76
+ retry = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
77
+ adapter = HTTPAdapter(max_retries=retry)
78
+ session.mount('https://', adapter)
79
+ session.mount('http://', adapter)
80
 
81
+ # Ensure patch is applied whenever we get a session
82
+ force_ipv4_requests()
83
+
84
+ return session
 
 
 
 
85