fix(auth,download): playlists/likes 401, suppression downloads, 429 formats
CI / build-and-test (push) Failing after 8m20s
CI / build-and-test (push) Failing after 8m20s
- likes + playlists: authMiddlewareCookieAware (cookies + Bearer) au lieu du Bearer seul -> corrige 401 Unauthorized meme connecte (creation playlist, ajout video, bouton J'aime) - add-to-playlist/like-button/search-box: retire HttpClientModule local qui court-circuitait les intercepteurs fonctionnels - intercepteur: refresh+retry sur 401 meme sans token en memoire (F5) - UserService: apiBase() au lieu de /proxy/api en dur (prod :4000) - J'aime: envoie titre/miniature, erreur visible au lieu de bouton mort - download: seaux separes (lectures 120/min, ecritures 30/min, formats 30/min) + reponses limiter en JSON rate_limited + Retry-After - formats: cache memoire 10 min + ytdlpNetOpts + retry front avec backoff - polling allege (downloads 5s, suivi job 4s, 429 transitoire non fatal) - erreurs FR precises (http-error.util): 401/403/404/410/429/quota/500 avec compte a rebours + bouton Reessayer
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
/** Code d'erreur métier renvoyé par le backend : { error: 'rate_limited', ... }. */
|
||||
export function backendErrorCode(err: unknown): string | null {
|
||||
try {
|
||||
const body = (err as HttpErrorResponse)?.error;
|
||||
if (body && typeof body === 'object' && typeof body.error === 'string') return body.error;
|
||||
if (typeof body === 'string') {
|
||||
const m = /"error"\s*:\s*"([a-z_]+)"/i.exec(body);
|
||||
if (m) return m[1];
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondes avant de réessayer après un 429 : backend { retryAfterSec } >
|
||||
* header Retry-After > header RateLimit-Reset (epoch secondes) > défaut.
|
||||
*/
|
||||
export function retryAfterSeconds(err: unknown, fallback = 30): number {
|
||||
try {
|
||||
const e = err as HttpErrorResponse;
|
||||
const body = e?.error;
|
||||
if (body && typeof body === 'object' && Number.isFinite(Number(body.retryAfterSec))) {
|
||||
return Math.max(1, Math.ceil(Number(body.retryAfterSec)));
|
||||
}
|
||||
const headers = e?.headers;
|
||||
const retryAfter = headers?.get?.('Retry-After');
|
||||
if (retryAfter && Number.isFinite(Number(retryAfter))) return Math.max(1, Math.ceil(Number(retryAfter)));
|
||||
const reset = headers?.get?.('RateLimit-Reset');
|
||||
if (reset && Number.isFinite(Number(reset))) {
|
||||
const nowSec = Date.now() / 1000;
|
||||
const wait = Math.ceil(Number(reset) - nowSec);
|
||||
if (wait >= 1 && wait <= 3600) return wait;
|
||||
}
|
||||
} catch {}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** Vrai quand l'échec est transitoire et mérite un retry (429 / réseau / 5xx). */
|
||||
export function isRetryableStatus(status: number | undefined): boolean {
|
||||
return status === 0 || status === 429 || (typeof status === 'number' && status >= 500 && status < 600);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message FR prêt à afficher pour les erreurs du module téléchargement.
|
||||
* Ne fuit jamais le corps brut (HTML / « Http failure response... »).
|
||||
*/
|
||||
export function frenchDownloadError(err: unknown): string {
|
||||
const e = err as HttpErrorResponse;
|
||||
const status = typeof e?.status === 'number' ? e.status : undefined;
|
||||
const code = backendErrorCode(err);
|
||||
if (status === 0) return 'Serveur de téléchargement injoignable. Vérifiez que le backend tourne puis réessayez.';
|
||||
if (status === 401) return 'Session expirée. Reconnectez-vous puis réessayez.';
|
||||
if (status === 403) {
|
||||
if (code === 'download_disabled_for_provider' || code === 'forbidden') {
|
||||
return code === 'forbidden'
|
||||
? 'Action interdite (ressource d’un autre compte).'
|
||||
: 'Le téléchargement est désactivé pour ce provider.';
|
||||
}
|
||||
return 'Action interdite.';
|
||||
}
|
||||
if (status === 404) return 'Ce téléchargement n’existe plus (déjà supprimé ou autre compte). Actualisez la liste.';
|
||||
if (status === 410) return 'Le fichier n’est plus présent sur le serveur (nettoyé). Relancez un téléchargement.';
|
||||
if (status === 429) {
|
||||
if (code === 'too_many_downloads') return 'Trop de téléchargements en parallèle. Attendez la fin d’un job puis réessayez.';
|
||||
if (code === 'storage_quota_exceeded') return 'Quota de stockage atteint. Supprimez d’anciens fichiers puis réessayez.';
|
||||
const wait = retryAfterSeconds(err);
|
||||
return `Trop de requêtes pour le moment (limite anti-abus). Réessayez dans ~${wait} s.`;
|
||||
}
|
||||
if (status === 400) {
|
||||
if (code === 'job_not_retryable') return 'Ce job ne peut pas être relancé dans son état actuel.';
|
||||
return 'Requête invalide.';
|
||||
}
|
||||
if (code === 'formats_failed' || status === 500) {
|
||||
return 'Impossible de récupérer les formats (service vidéo indisponible). Réessayez dans un instant.';
|
||||
}
|
||||
return 'Opération impossible pour le moment. Réessayez plus tard.';
|
||||
}
|
||||
Reference in New Issue
Block a user