From 1a75433281c4f62f0aa3b3d6936015877752d5b5 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Thu, 13 Aug 2026 18:16:54 +0200 Subject: [PATCH] fix(proxy): rozdziel wygasly podpis od awarii CDN + ponow przejsciowe 5xx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zgloszenie 7f2234eb: "Cos nie tak z ekstraktorem? Bo samo wideo dziala a aplikacja zglasza problem z hostem" (scena galaxyporn). Ekstraktor jest SPRAWNY. Sprawdzone end-to-end: resolve 200, master m3u8, wariant, 525 segmentow, 60/60 pobranych. Telemetria 30 dni dla tego zrodla: 1310 sukcesow, 25 bledow (1,9%). Ale przy okazji wyszlo cos wiekszego. Wszystkie te bledy to "502", a mapowanie w `_upstream_error_response` sklejalo w ten kod DWIE rozne awarie: upstream 5xx (CDN padl) ORAZ kazde inne 4xx, w tym 403. Test syntetyczny: upstream oddaje 403, nasze proxy oddawalo 502. To bolalo podwojnie. `playback_events.error_detail` bierze sie z komunikatu ExoPlayera, wiec w telemetrii nie dalo sie odroznic "CDN padl" od "wygasl podpisany token" — a to zupelnie inne naprawy (pierwsze: czekac, drugie: odswiezyc resolve). Do tego 403 szedl przez raise jako "moze nasz bug" i zasmiecal Sentry, choc wygasanie tokenow na tych CDN-ach jest NORMALNE. Zmiany: 1. 401/403 przepuszczane z prawdziwym kodem (+ log INFO, bez wyjatku). Cache i tak byl juz invalidowany na 401/403/404/410, wiec kolejny resolve dostaje swiezy URL. 2. Przejsciowe 502/503/504 ponawiane RAZ (tylko GET/HEAD, idempotentne). Te CDN-y stoja na wielu wezlach (galaxyporn: 3 rozne IP w jednej sesji), wiec druga proba zwykle trafia w zdrowy. 4xx nie ponawiamy. Zweryfikowane: 403 wychodzi teraz jako 403 (bylo 502), normalne odtwarzanie bez zmian (60/60 segmentow 200). Uwaga: sciezka ponowienia nie zostala odpalona end-to-end, bo 502 nie da sie wywolac na zadanie. Widac ja w logu ("proxy: upstream N, ponawiam"). --- app/api/stream_proxy.py | 48 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) 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}")