digifreely commited on
Commit
55194e0
Β·
verified Β·
1 Parent(s): a27072c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -16
app.py CHANGED
@@ -7,6 +7,7 @@ then asynchronously forwards the full payload to the appropriate downstream URL.
7
 
8
  import os
9
  import json
 
10
  import logging
11
  from contextlib import asynccontextmanager
12
 
@@ -205,25 +206,94 @@ async def block_ip_cloudflare(ip: str) -> None:
205
  # Downstream forwarding helper
206
  # ──────────────────────────────────────────────
207
  async def forward_request(target_url: str, payload: dict, serv_code: str) -> tuple[dict, int]:
208
- """POST the full payload to the chosen downstream service."""
 
 
 
 
209
  fwd_headers = {
210
  "Content-Type": "application/json",
211
  "serv_code": serv_code,
212
  }
213
- try:
214
- async with httpx.AsyncClient(timeout=120.0) as client:
215
- resp = await client.post(target_url, json=payload, headers=fwd_headers)
216
- logger.info("Downstream %s β†’ HTTP %s", target_url, resp.status_code)
217
- try:
218
- return resp.json(), resp.status_code
219
- except Exception:
220
- return {"raw_response": resp.text}, resp.status_code
221
- except httpx.TimeoutException:
222
- logger.error("Timeout forwarding to %s", target_url)
223
- return {"error": f"Downstream timeout: {target_url}"}, 504
224
- except Exception as exc:
225
- logger.error("Error forwarding to %s: %s", target_url, exc)
226
- return {"error": str(exc)}, 502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
 
229
  # ──────────────────────────────────────────────
@@ -320,4 +390,4 @@ async def chat(request: Request):
320
  "decision": decision,
321
  "forwarded": target_url,
322
  "response": response_body,
323
- })
 
7
 
8
  import os
9
  import json
10
+ import asyncio
11
  import logging
12
  from contextlib import asynccontextmanager
13
 
 
206
  # Downstream forwarding helper
207
  # ──────────────────────────────────────────────
208
  async def forward_request(target_url: str, payload: dict, serv_code: str) -> tuple[dict, int]:
209
+ """
210
+ POST the full payload to the chosen downstream service.
211
+ Retries up to 3 times on ConnectError or TimeoutException with exponential backoff.
212
+ Uses split timeouts to handle Render cold-starts gracefully.
213
+ """
214
  fwd_headers = {
215
  "Content-Type": "application/json",
216
  "serv_code": serv_code,
217
  }
218
+
219
+ # Split timeouts: generous connect window for Render cold-starts,
220
+ # generous read window for slow inference on downstream services.
221
+ timeout = httpx.Timeout(
222
+ connect=60.0, # Render free-tier cold-start can take 30–50 s
223
+ read=120.0, # downstream inference may be slow
224
+ write=30.0,
225
+ pool=10.0,
226
+ )
227
+
228
+ last_exc: Exception | None = None
229
+
230
+ for attempt in range(1, 4): # attempts 1, 2, 3
231
+ try:
232
+ logger.info(
233
+ "Forward attempt %d/3 β†’ %s", attempt, target_url
234
+ )
235
+ async with httpx.AsyncClient(timeout=timeout) as client:
236
+ resp = await client.post(target_url, json=payload, headers=fwd_headers)
237
+ logger.info(
238
+ "Downstream %s β†’ HTTP %s (attempt %d)", target_url, resp.status_code, attempt
239
+ )
240
+ try:
241
+ return resp.json(), resp.status_code
242
+ except Exception as parse_exc:
243
+ logger.warning(
244
+ "Could not parse JSON from %s (HTTP %s): %s β€” returning raw text",
245
+ target_url, resp.status_code, parse_exc,
246
+ )
247
+ return {"raw_response": resp.text}, resp.status_code
248
+
249
+ except httpx.TimeoutException as exc:
250
+ last_exc = exc
251
+ logger.warning(
252
+ "Attempt %d/3 TIMEOUT for %s | type=%s | detail=%s",
253
+ attempt, target_url, type(exc).__name__, exc,
254
+ )
255
+
256
+ except httpx.ConnectError as exc:
257
+ # This is the primary cause of 502s with Render cold-starts:
258
+ # the service is sleeping and refuses/resets the connection.
259
+ last_exc = exc
260
+ logger.warning(
261
+ "Attempt %d/3 CONNECT ERROR for %s | type=%s | detail=%s",
262
+ attempt, target_url, type(exc).__name__, exc,
263
+ )
264
+
265
+ except httpx.HTTPStatusError as exc:
266
+ # Downstream returned a 4xx/5xx β€” no point retrying.
267
+ logger.error(
268
+ "Downstream %s returned HTTP error (attempt %d): status=%s body=%s",
269
+ target_url, attempt, exc.response.status_code, exc.response.text,
270
+ )
271
+ return {"error": f"Downstream HTTP error: {exc.response.status_code}"}, exc.response.status_code
272
+
273
+ except Exception as exc:
274
+ # Unexpected / non-retryable error β€” log full traceback and bail.
275
+ logger.exception(
276
+ "Attempt %d/3 UNEXPECTED ERROR for %s | type=%s | detail=%s",
277
+ attempt, target_url, type(exc).__name__, exc,
278
+ )
279
+ return {"error": f"Unexpected forwarding error: {exc}"}, 502
280
+
281
+ # Exponential backoff before next attempt (2 s, 4 s)
282
+ if attempt < 3:
283
+ backoff = 2 ** attempt
284
+ logger.info("Backing off %ds before retry …", backoff)
285
+ await asyncio.sleep(backoff)
286
+
287
+ # All 3 attempts exhausted
288
+ logger.error(
289
+ "All 3 forward attempts failed for %s | last_error_type=%s | last_error=%s",
290
+ target_url, type(last_exc).__name__, last_exc,
291
+ )
292
+ return {
293
+ "error": f"Downstream unreachable after 3 attempts: {target_url}",
294
+ "last_error_type": type(last_exc).__name__,
295
+ "last_error": str(last_exc),
296
+ }, 502
297
 
298
 
299
  # ──────────────────────────────────────────────
 
390
  "decision": decision,
391
  "forwarded": target_url,
392
  "response": response_body,
393
+ })