Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
fwd_headers = {
|
| 210 |
"Content-Type": "application/json",
|
| 211 |
"serv_code": serv_code,
|
| 212 |
}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
})
|