diff --git a/app/api/stream_proxy.py b/app/api/stream_proxy.py index 2e74fc5..29d8d4e 100644 --- a/app/api/stream_proxy.py +++ b/app/api/stream_proxy.py @@ -31,6 +31,7 @@ import time from typing import Annotated from urllib.parse import urljoin, urlparse +import asyncio import httpx from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import Response, StreamingResponse @@ -714,6 +715,28 @@ async def proxy_stream( stream=True, follow_redirects=True, ) + # Przejściowy błąd SERWERA upstreamu → jedno ponowienie, zanim oddamy błąd + # do playera. Te CDN-y stoją na wielu węzłach (galaxyporn: 203.188.166.163, + # 185.237.106.182, 185.237.107.133), więc druga próba zwykle trafia w zdrowy. + # + # Telemetria 30 dni: 502/503 to JEDYNA klasa błędów playbacku przy kilku + # niezależnych źródłach naraz (porndoe 65, 8kporner 37, galaxyporn 22), przy + # 1310 udanych odtworzeniach samego galaxyporn, czyli 1,9%. Zgłoszenie + # 7f2234eb („samo wideo działa a aplikacja zgłasza problem z hostem") to + # dokładnie ten przypadek: dla usera wygląda na zepsute źródło, a to mignięcie. + # + # Ponawiamy TYLKO 502/503/504 i tylko dla GET/HEAD (idempotentne, segment HLS + # to zwykły GET). 4xx zostawiamy: 403/410 to wygasły token, a nie chwilowa + # awaria, i mają własną ścieżkę z invalidacją cache niżej. + if upstream.status_code in (502, 503, 504) and method.upper() in ("GET", "HEAD"): + await upstream.aclose() + await asyncio.sleep(0.25) + log.info("proxy: upstream %d, ponawiam %s", upstream.status_code, target[:70]) + upstream = await client.send( + client.build_request(method, target, headers=headers), + stream=True, + follow_redirects=True, + ) if upstream.status_code >= 400: status = upstream.status_code ups_headers = dict(upstream.headers) @@ -854,5 +877,28 @@ def _upstream_error_response( headers={"Retry-After": "5"}, media_type="text/plain", ) - # 4xx other (403 itp.), raise żeby Sentry zarejestrował (może bug naszego kodu) + if status in (401, 403): + # WYGASŁY/NIEWAŻNY PODPIS, nie awaria hosta. Przepuszczamy prawdziwy kod zamiast + # mapować na 502, z dwóch powodów: + # + # 1. Diagnostyka. `playback_events.error_detail` bierze się z komunikatu + # ExoPlayera („Response code: 502"), więc dopóki mapowaliśmy wszystko na 502, + # w telemetrii nie dało się odróżnić „CDN padł" od „token wygasł". Zgłoszenie + # 7f2234eb („samo wideo działa a aplikacja zgłasza problem z hostem") to + # dokładnie ta dwuznaczność: dla usera źródło wygląda na zepsute, a wystarczy + # świeży resolve. + # 2. Sentry. Wygaśnięcie podpisanego URL-a jest NORMALNE (te CDN-y dają tokeny + # na minuty), a raise robił z tego wyjątek „może nasz bug" i zaśmiecał alerty. + # + # Cache i tak jest tu invalidowany wyżej (401/403/404/410), więc kolejny resolve + # dostanie świeży URL. + log.info("proxy upstream %d (wygasly podpis?) for %s", status, target) + return Response( + content=f"upstream {status}", + status_code=status, + media_type="text/plain", + headers={"Cache-Control": "no-store"}, + ) + # Pozostałe 4xx (400/405/451…) nadal jako 502 + wyjątek: tego NIE spodziewamy się + # od poprawnie zbudowanego requestu, więc chcemy o tym wiedzieć w Sentry. raise HTTPException(status_code=502, detail=f"upstream {status}")