fix(rumble): un HTTP 404 net n'est pas un blocage Cloudflare
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
Probe live depuis le conteneur : rumble.com renvoie 404 (vraie page, non-challenge) pour un id inexistant. fetchHtml les classait en échec total, d'où deux régressions : 503 sur /video (les vidéos mortes ne sont plus retirées des Shorts) et armement du cooldown 60 s pour toute la maison au premier id bogus. - fetchHtml retourne toute réponse nette non-challenge ; le statut est jugé par les appelants (tous dans le même fichier). - rumbleFailureForStatus() exporté : 403 -> 503 rumble_cloudflare_challenge (bandeau identique à la recherche), 404/410 -> 404 rumble_not_found, 5xx -> 503 rumble_upstream_error. /video, /browse et /shorts passent par ce mapping. - handler.search ne parse plus que du HTTP 200 : les pages 404 de Rumble embarquent des cartes de recommandation qui seraient autrement prises pour des résultats de recherche. - Tests : 7 assertions sur le mapping (sans réseau), 64 au total.
This commit is contained in:
@@ -138,7 +138,11 @@ async function fetchHtml(url) {
|
||||
if (Date.now() < fetchCooldownUntil) return null;
|
||||
try {
|
||||
const r = await nodeFetch(url);
|
||||
if (r.status === 200 && !isChallenge(r.html)) return r;
|
||||
// Toute réponse NETTE non-challenge est retournée telle quelle (200, mais
|
||||
// aussi 404 « ressource absente » : rumble.com répond 404 avec une vraie
|
||||
// page, cf. probe container). Le statut est jugé par les appelants — armer
|
||||
// le cooldown sur un 404 empoisonnerait le circuit breaker 60 s pour tous.
|
||||
if (r.status && !isChallenge(r.html)) return r;
|
||||
} catch { /* fallback */ }
|
||||
const py = await pythonFetch(url);
|
||||
if (py) return py;
|
||||
@@ -416,6 +420,19 @@ function rumbleFail(code, status, { challenged = true } = {}) {
|
||||
return Object.assign(new Error(code), { code, status, rumbleChallenged: challenged });
|
||||
}
|
||||
|
||||
/**
|
||||
* Statut HTTP rumble.com -> erreur typée, partagé par /video, /browse et
|
||||
* /shorts : 403 = blocage (mêmes mots-clés que les erreurs de recherche =>
|
||||
* même bandeau « bloqué temporairement »), 404/410 = ressource réellement
|
||||
* absente (404 côté Shorts = retrait du flux, c'est le contrat), 5xx = amont
|
||||
* cassé. Exporté pour que ce contrat soit testable sans réseau.
|
||||
*/
|
||||
export function rumbleFailureForStatus(status) {
|
||||
if (status === 403) return rumbleFail('rumble_cloudflare_challenge', 503);
|
||||
if (status === 404 || status === 410) return rumbleFail('rumble_not_found', 404, { challenged: false });
|
||||
return rumbleFail('rumble_upstream_error', 503, { challenged: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Surcouche JSON-LD partagée (phase 3.1) : complète ce que le DOM n'expose pas
|
||||
* (date de publication, vues, auteur) SANS jamais écraser une valeur réellement
|
||||
@@ -537,6 +554,7 @@ export async function scrapeRumbleVideo(videoIdOrUrl) {
|
||||
const fetchUrl = norm?.urlCanonique || `https://rumble.com/${videoIdOrUrl}`;
|
||||
const r = await fetchHtml(fetchUrl);
|
||||
if (!r || isChallenge(r.html)) throw rumbleFail('rumble_cloudflare_challenge', 503);
|
||||
if (r.status !== 200) throw rumbleFailureForStatus(r.status);
|
||||
|
||||
const $ = load(r.html);
|
||||
const ident = extractVideoIdentity($);
|
||||
@@ -732,6 +750,7 @@ export async function scrapeRumbleList({ page = 1, limit = 24, sort = 'viral', m
|
||||
: `https://rumble.com/videos?sort=${encodeURIComponent(String(sort))}&page=${pageNum}`;
|
||||
const r = await fetchHtml(url);
|
||||
if (!r || isChallenge(r.html)) throw rumbleFail('rumble_cloudflare_challenge', 503);
|
||||
if (r.status !== 200) throw rumbleFailureForStatus(r.status);
|
||||
|
||||
const items = parseCardsHtml(r.html, { mode });
|
||||
const list = items.slice(0, limitNum);
|
||||
@@ -776,7 +795,7 @@ const handler = {
|
||||
const params = new URLSearchParams({ q: query });
|
||||
if (pageNum > 1) params.set('page', String(pageNum));
|
||||
const r = await fetchHtml(`https://rumble.com/search/video?${params.toString()}`);
|
||||
if (r && !isChallenge(r.html)) {
|
||||
if (r?.status === 200 && !isChallenge(r.html)) {
|
||||
const items = parseSearchHtml(r.html, { limit: perPage });
|
||||
if (items.length > 0) return items;
|
||||
}
|
||||
@@ -787,7 +806,7 @@ const handler = {
|
||||
const params = new URLSearchParams({ 'search-videos': '1', q: query });
|
||||
if (pageNum > 1) params.set('page', String(pageNum));
|
||||
const r = await fetchHtml(`https://rumble.com/search/all?${params.toString()}`);
|
||||
if (r && !isChallenge(r.html)) {
|
||||
if (r?.status === 200 && !isChallenge(r.html)) {
|
||||
const items = parseSearchHtml(r.html, { limit: perPage });
|
||||
if (items.length > 0) return items;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseJsonLd, parseRumbleViews, parseSearchHtml, resetRumbleNegativeCache, parseCardsHtml, scrapeRumbleList, scrapeRumbleVideo, armRumbleFetchCooldown, resetRumbleFetchCooldown } from '../providers/rumble.mjs';
|
||||
import { parseJsonLd, parseRumbleViews, parseSearchHtml, resetRumbleNegativeCache, parseCardsHtml, scrapeRumbleList, scrapeRumbleVideo, armRumbleFetchCooldown, resetRumbleFetchCooldown, rumbleFailureForStatus } from '../providers/rumble.mjs';
|
||||
import { parseRelativeDate } from '../providers/youtube-innertube.mjs';
|
||||
import { itemDurationSec } from '../search-filters.mjs';
|
||||
|
||||
@@ -181,4 +181,14 @@ await expectFail(() => scrapeRumbleList({}), 503, 'rumble_cloudflare_challenge',
|
||||
resetRumbleFetchCooldown();
|
||||
pass++; console.log(' V cooldown CF: /video et /browse échouent en 503 sans réseau');
|
||||
|
||||
// --- Mapping statut HTTP -> erreur typée (rumble.com renvoie 404 NET pour un
|
||||
// id inexistant : ce n'est PAS un blocage, ça ne doit pas armer le cooldown) ---
|
||||
eq(rumbleFailureForStatus(404).status, 404, 'statut 404 rumble -> 404 (absente, pas un blocage)');
|
||||
eq(rumbleFailureForStatus(404).code, 'rumble_not_found', 'statut 404 rumble -> code rumble_not_found');
|
||||
eq(rumbleFailureForStatus(410).status, 404, 'statut 410 -> 404');
|
||||
eq(rumbleFailureForStatus(403).status, 503, 'statut 403 -> 503 bloqué');
|
||||
eq(rumbleFailureForStatus(403).code, 'rumble_cloudflare_challenge', '403 -> même code que le blocage de recherche (bandeau identique)');
|
||||
eq(rumbleFailureForStatus(500).status, 503, 'statut 5xx -> 503 amont');
|
||||
eq(rumbleFailureForStatus(500).code, 'rumble_upstream_error', '5xx -> code amont');
|
||||
|
||||
console.log(`\n rumble-ld: ${pass} assertions OK`);
|
||||
|
||||
Reference in New Issue
Block a user