fix(proxy): rozdziel wygasly podpis od awarii CDN + ponow przejsciowe 5xx

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").
This commit is contained in:
goon-foss 2026-08-13 18:16:54 +02:00
parent dde21f2e6e
commit 1a75433281

View file

@ -31,6 +31,7 @@ import time
from typing import Annotated from typing import Annotated
from urllib.parse import urljoin, urlparse from urllib.parse import urljoin, urlparse
import asyncio
import httpx import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import Response, StreamingResponse from fastapi.responses import Response, StreamingResponse
@ -714,6 +715,28 @@ async def proxy_stream(
stream=True, stream=True,
follow_redirects=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: if upstream.status_code >= 400:
status = upstream.status_code status = upstream.status_code
ups_headers = dict(upstream.headers) ups_headers = dict(upstream.headers)
@ -854,5 +877,28 @@ def _upstream_error_response(
headers={"Retry-After": "5"}, headers={"Retry-After": "5"},
media_type="text/plain", 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}") raise HTTPException(status_code=502, detail=f"upstream {status}")