Filipes Kachel ging nicht. Der Grund war nicht TikTok, sondern EIN WORT
in unserer Kennung.
DAS WORT HEISST "Mobile"
Gestern hiess es hier noch "17 KB, keine Daten, von jedem Weg aus".
Nachgemessen mit einem Keksglas und fuenf Kennungen:
Desktop-Kennung, ohne Kekse 17 KB keine Daten
Desktop-Kennung, MIT Keksen 17 KB keine Daten
Android-Kennung, ohne Kekse 939 KB DATEN
iPhone-Kennung, mit Keksen 939 KB DATEN
TikTok-App, mit Keksen 42 KB keine Daten
TikTok baut die Seite serverseitig NUR fuer die Handyfassung auf; am
Rechner laedt sie sich mit JavaScript nach. Kekse, Sprache und
Accept spielen keine Rolle -- einzeln nachgemessen.
UND WIR BLEIBEN EHRLICH: Unser Name und unsere Adresse stehen hinten
an der Kennung. Nachgemessen liefert TikTok damit dieselben 939 KB.
Es kostet also nichts. Die Pruefung verlangt jetzt BEIDES: "Mobile"
(sonst kommt nichts an) und unseren Namen (sonst ist es eine
Verkleidung).
ZWEI VORLAGEN, NICHT EINE
Filipes Einladung fuehrt zu "LIVE Glow Up" -- und die ist ganz
anders gebaut als "Gipfelstuermer". Ihr fehlen gleich DREI
Bausteine, auf denen der Bauplan beruht: live_rule_introduction,
live_reward_introduction, live_campaign_intro. Stattdessen hat sie
live_task_group (Aufgaben als Reiter) und live_secondary_gift.
Der Leser kann jetzt beide. Gemessen:
Gipfelstuermer 5 Aufgaben, 5 Geschenke, 6 Preisgruppen, 0 Hinweise
LIVE Glow Up 2 Aufgaben, 1 Geschenk, 0 Preise, 2 Hinweise
Die doppelten Reiter fallen weg: Die Vorlage legt je Reiter zwei
Eintraege an (Creator und Zuschauer), und zweimal derselbe Haken
waere ein Haken an zwei Stellen.
"HAT KEINE" IST ETWAS ANDERES ALS "KONNTE NICHT GELESEN WERDEN"
Ohne diese Unterscheidung haette LIVE Glow Up vier Hinweise gemeldet,
die alle nach einem Fehler klingen -- obwohl schlicht nichts da ist.
Das eine schickt jemanden suchen, das andere sagt ihm, dass nichts
fehlt. Entsprechend haengt der Regeltext auch kein pauschales "Bitte
nachtragen" mehr an: Jeder Hinweis sagt selbst, ob etwas zu tun ist.
FUENFTE PRUEFDATEI
kampagne-aufgabengruppe.html.br -- die echte Seite von LIVE Glow Up,
unveraendert, unangemeldet geholt (uid "0", anchor_id leer),
147 KB gepackt, sofort zurueckgelesen und Byte fuer Byte verglichen.
Ohne sie koennte der Leser eine Vorlage und faellt bei der naechsten
Einladung um.
GEPRUEFT
pruef-kampagne-lesen 112 -> 128 (eigener Abschnitt fuer die zweite
Vorlage, mit Gegenprobe: Gipfelstuermer meldet weiterhin nichts) ·
pruef-kampagne 102 -> 103 · pruef-struktur, pruef-zeichen, pruef-video
Co-Authored-By: Claude Opus 5 <[email protected]>
373 lines
16 KiB
JavaScript
373 lines
16 KiB
JavaScript
/* =====================================================================
|
|
DER WEG NACH DRAUSSEN ZU TIKTOK — gemeinsam genutzt (07.10.2026)
|
|
|
|
Diese Datei begann als ein VERSCHIEBEN, keine Neuerfindung.
|
|
`istTikTok` und `holeMitFrist` standen bis heute in
|
|
`workspace-video.js` und haben sich dort bewährt — inklusive der
|
|
Reparatur vom 05.10.2026, bei der auffiel, dass die Frist nur den
|
|
Anfang der Anfrage bewachte.
|
|
|
|
Ab jetzt holt auch die Kampagnen-Kachel eine Seite von TikTok. Zwei
|
|
Fassungen derselben Regel wären der Fehler, den CLAUDE.md am
|
|
11.09.2026 beschreibt: eine abgeschriebene Liste, die auseinander-
|
|
läuft. Gerade bei `istTikTok` wäre das gefährlich — es ist die eine
|
|
Stelle, die entscheidet, welchen fremden Rechner unser Server
|
|
überhaupt anfragt. Die zweite Fassung würde beim nächsten Fund
|
|
vergessen.
|
|
|
|
---------------------------------------------------------------------
|
|
WAS SICH BEIM VERSCHIEBEN GEÄNDERT HAT — und nur das
|
|
|
|
1. `holeMitFrist` nimmt die Frist jetzt als Zusatz entgegen, mit
|
|
8000 ms als Vorgabe. Das Video bleibt damit Zeichen für Zeichen
|
|
beim alten Verhalten; die Kampagnenseite braucht mehr (1,1 MB
|
|
statt einer kleinen Auskunft) und sagt das am Aufrufort, nicht
|
|
über eine zweite Konstante irgendwo.
|
|
|
|
2. `istTikTok` erkennt die Prüfumgebung jetzt an BEIDEN
|
|
Umstellschaltern — `TIKTOK_OEMBED_BASIS` (Video) und
|
|
`TIKTOK_KAMPAGNE_BASIS` (Kampagne). Ohne das zweite müsste die
|
|
Kampagnenprüfung die echte Regel umgehen und prüfte damit eine
|
|
andere als die, die im Betrieb gilt.
|
|
|
|
---------------------------------------------------------------------
|
|
KEINE DATENBANK, KEIN EXPRESS
|
|
|
|
Absichtlich: Diese Datei lässt sich in einer Prüfung einzeln
|
|
einbinden, ohne dass ein Server hochfährt. Dasselbe Argument wie bei
|
|
`helfer-tag.mjs`.
|
|
===================================================================== */
|
|
|
|
/* GRENZEN FÜR DEN WEG NACH DRAUSSEN. Ein fremder Dienst, der nicht
|
|
antwortet, darf unsere Anfrage nicht mit sich ziehen -- genau daran
|
|
ist in diesem Haus schon ein ganzer Pruefdurchlauf drei Stunden lang
|
|
haengengeblieben. */
|
|
export const FRIST_MS = 8000;
|
|
|
|
/** Ist das ueberhaupt eine TikTok-Adresse? Keine Zeichenkettensuche,
|
|
* sondern der Rechnername -- "tiktok.com.beispiel.de" enthaelt
|
|
* "tiktok.com" und gehoert jemand anderem. */
|
|
export function istTikTok(roh) {
|
|
try {
|
|
const u = new URL(String(roh));
|
|
const h = u.hostname.toLowerCase();
|
|
/* NUR IN EINER PRUEFUNG: Dann liegt der nachgebaute Dienst auf
|
|
127.0.0.1, und die Adresse des "Videos" auch. Ohne diese Zeile
|
|
muesste die Pruefung die echte Regel umgehen -- und pruefte
|
|
damit eine andere als die, die im Betrieb gilt. */
|
|
if ((process.env.TIKTOK_OEMBED_BASIS || process.env.TIKTOK_KAMPAGNE_BASIS)
|
|
&& (h === "127.0.0.1" || h === "localhost")) {
|
|
return true;
|
|
}
|
|
if (u.protocol !== "https:") return false;
|
|
return h === "tiktok.com" || h.endsWith(".tiktok.com");
|
|
} catch { return false; }
|
|
}
|
|
|
|
/* =====================================================================
|
|
DER WEG ZU EINER GANZEN SEITE (07.10.2026) — Etappe 2
|
|
|
|
Bis hierher holte dieses Haus bei TikTok nur kleine Auskuenfte: eine
|
|
JSON-Antwort zu einem Video, ein Vorschaubild. Eine Kampagnenseite
|
|
ist etwas anderes -- 1,1 MB serverseitig gerendertes HTML, und die
|
|
Adresse dahin kommt aus einem Text, den ein Mensch eingefuegt hat.
|
|
|
|
DAS IST DIE EINZIGE STELLE IM GANZEN HAUS, an der unser Server eine
|
|
FREMDBESTIMMTE Adresse abruft. Ueberall sonst steht das Ziel im
|
|
Quelltext. Deshalb stehen hier drei Grenzen, und jede hat einen
|
|
gemessenen Grund:
|
|
|
|
1. JEDE STATION EINZELN.
|
|
holeMitFrist folgt Umleitungen selbst (redirect "follow"). Das
|
|
heisst: istTikTok() hat nur die EINGABE gesehen, und wohin der
|
|
Kurzlink wirklich fuehrt, weiss niemand. Hier wird deshalb
|
|
Sprung fuer Sprung gegangen (redirect "manual", hoechstens
|
|
fuenf), und JEDE Zwischenadresse geht durch istTikTok().
|
|
Verlaesst die Kette TikTok, bricht sie ab und sagt, wohin.
|
|
|
|
2. ZWOELF SEKUNDEN, NICHT ACHT.
|
|
Gemessen am 07.10.2026: allein 750 ms beim Ursprungsserver, dazu
|
|
die Uebertragung von 1,1 MB. Acht Sekunden reichen an einem
|
|
langsamen Nachmittag nicht, und eine Frist, die im Normalfall
|
|
zuschlaegt, ist keine Grenze, sondern ein Fehler.
|
|
|
|
3. DREI MEGABYTE, GEZOGEN BEIM LESEN.
|
|
Nicht hinterher geprueft. Wer erst arrayBuffer() abwartet und
|
|
dann die Laenge ansieht, hat die 50 MB schon im Speicher, bevor
|
|
er sie ablehnt -- die Grenze schuetzt dann genau nichts. Gelesen
|
|
wird deshalb stueckweise, und beim Ueberschreiten wird die
|
|
Verbindung abgebrochen.
|
|
|
|
Dazu ein ehrlicher Erkennungsname: Wer gefragt wird, soll sehen,
|
|
wer fragt. Ein Abruf, der sich als Browser ausgibt, ist eine Luege
|
|
gegenueber einem Dienst, der uns eingeladen hat.
|
|
===================================================================== */
|
|
|
|
/* Fuer eine ganze Seite. Siehe Grund 2 oben. */
|
|
export const FRIST_SEITE_MS = 12_000;
|
|
/* Siehe Grund 3. 1,1 MB ist der gemessene Normalfall, 3 MB lassen
|
|
Luft fuer eine Kampagne mit mehr Bausteinen -- und sind weit unter
|
|
dem, was einem Server wehtut. */
|
|
export const SEITE_MAX = 3 * 1024 * 1024;
|
|
/* Hoechstens fuenf Spruenge. Ein Kurzlink braucht einen, TikToks
|
|
Weiterleitung auf die Langform einen zweiten. Wer fuenf braucht,
|
|
hat kein Ziel, sondern eine Schleife. */
|
|
export const UMLEITUNGEN_MAX = 5;
|
|
|
|
/* WER FRAGT DA -- UND WELCHE FASSUNG DER SEITE WIR WOLLEN
|
|
(07.10.2026, gemessen nachdem es NICHT lief).
|
|
|
|
Erst stand hier nur unser eigener Name. Damit kam von TikTok eine
|
|
leere Huelle: 17 KB, Titel "Campaign", kein Inhalt. Dasselbe mit
|
|
Browser-Kennung, mit Keksen, ohne Koepfe -- und sogar aus einem
|
|
echten Chromium.
|
|
|
|
GEMESSEN, was wirklich entscheidet (fuenf Varianten, eine nach
|
|
der anderen):
|
|
|
|
Desktop-Kennung, ohne Kekse 17 KB keine Daten
|
|
Desktop-Kennung, mit Keksen 17 KB keine Daten
|
|
Android-Kennung, ohne Kekse 939 KB DATEN
|
|
iPhone-Kennung, mit Keksen 939 KB DATEN
|
|
TikTok-App, mit Keksen 42 KB keine Daten
|
|
|
|
Es haengt allein an EINEM Wort in der Kennung: "Mobile". TikTok
|
|
baut die Seite serverseitig nur fuer die Handyfassung auf -- am
|
|
Rechner laedt sie sich mit JavaScript nach. Kekse, Sprache und
|
|
Accept spielen keine Rolle (einzeln nachgemessen).
|
|
|
|
UND WIR BLEIBEN TROTZDEM EHRLICH. Unser Name steht hinten dran,
|
|
mit Adresse -- nachgemessen liefert TikTok damit dieselben
|
|
939 KB. Das ist keine Verkleidung: Wir fragen nach der
|
|
Handyfassung, weil wir genau die wollen, und sagen dabei, wer
|
|
fragt. Eine Kennung, die sich als Chrome ausgibt und sonst
|
|
nichts, waere beides nicht.
|
|
|
|
WENN ES WIEDER KIPPT: Die Zeile unten ist der erste Ort zum
|
|
Nachsehen. Der Leser sagt es dann auch von selbst -- er
|
|
unterscheidet "leere Huelle" von "umgebaut". */
|
|
export const KENNUNG =
|
|
"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 "
|
|
+ "(KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36 "
|
|
+ "DogfatherUniverse-Workspace/1.0 "
|
|
+ "(+https://workspace.dogfather-universe.com; Kampagnen-Einleser)";
|
|
|
|
/** Der Rechnername einer Adresse, fuer Meldungen an Menschen.
|
|
* Gibt einen Ausschnitt der Eingabe zurueck, wenn sie keine ist. */
|
|
export function hostVon(roh) {
|
|
try { return new URL(String(roh)).hostname; } catch { return String(roh).slice(0, 60); }
|
|
}
|
|
|
|
/* Adressen in einem Fliesstext. Das Muster FINDET nur -- geprueft wird
|
|
mit new URL() und istTikTok(). Ein regulaerer Ausdruck, der
|
|
entscheidet, ob eine Adresse zu TikTok gehoert, ist derselbe Fehler
|
|
wie eine Zeichenkettensuche nach "tiktok.com". */
|
|
const LINK_MUSTER = /https?:\/\/[^\s"'<>)\]}]+/gi;
|
|
|
|
/** Die erste TikTok-Adresse in einem Text -- oder null.
|
|
*
|
|
* WARUM DER GANZE TEXT HINEINDARF: Die Einladung kommt so, wie TikTok
|
|
* sie schickt, mit Fliesstext drumherum:
|
|
*
|
|
* Dein Creator Network laedt dich ein, an einer Kampagne
|
|
* teilzunehmen. Klicke hier, um weitere Informationen zu
|
|
* erhalten: https://www.tiktok.com/t/ZSbqPsFa8/
|
|
*
|
|
* Niemand soll den Link erst heraussuchen muessen. */
|
|
export function linkAusText(text) {
|
|
for (const treffer of String(text || "").matchAll(LINK_MUSTER)) {
|
|
/* Satzzeichen am Ende gehoeren zum Satz, nicht zur Adresse --
|
|
mit einem Punkt dahinter waere es eine andere Seite. */
|
|
const sauber = treffer[0].replace(/[.,;:!?"'»«)\]]+$/u, "");
|
|
if (istTikTok(sauber)) return sauber;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/* Was NICHT abgerufen wird, egal wer es vorschlaegt. Buchstabenform,
|
|
keine Namensaufloesung: Eine Pruefung, die erst DNS fragt, kann
|
|
zwischen Frage und Abruf ueberholt werden (DNS-Rebinding). Was
|
|
hier steht, faengt den realistischen Fall -- eine Adresse, die auf
|
|
unseren eigenen Rechner oder ins Hausnetz zeigt. */
|
|
const PRIVATE_NAMEN = /^(localhost|.*\.local|.*\.internal|.*\.home\.arpa)$/i;
|
|
const PRIVATE_ZAHLEN = new RegExp([
|
|
"^127\\.", // Rueckschleife
|
|
"^10\\.", // privates Netz
|
|
"^192\\.168\\.", // privates Netz
|
|
"^172\\.(1[6-9]|2\\d|3[01])\\.", // privates Netz
|
|
"^169\\.254\\.", // Link-lokal, darunter 169.254.169.254
|
|
"^0\\.", "^::1$", "^\\[?::1\\]?$", "^f[cd]", "^fe80:",
|
|
].join("|"), "i");
|
|
|
|
/** Darf unser Server dieses BILD holen?
|
|
*
|
|
* WARUM ES DIESE FRAGE GIBT: Die Banneradresse steht in der Seite,
|
|
* die TikTok ausliefert -- sie ist FREMDBESTIMMT. Ohne Schranke
|
|
* wuerde unser Server abrufen, was dort steht, auch
|
|
* http://127.0.0.1:4100/... oder http://169.254.169.254/ (die
|
|
* Metadatenadresse vieler Rechenzentren). Das ist der Lehrbuchfall
|
|
* SSRF, und er kostet nichts zu verhindern.
|
|
*
|
|
* WAS DAS NICHT KANN: Ein Name, der auf eine private Adresse
|
|
* zeigt, faellt hier nicht auf -- dafuer muesste aufgeloest werden,
|
|
* und zwischen Aufloesung und Abruf laesst sich die Antwort
|
|
* tauschen. Das steht hier, damit niemand mehr Schutz annimmt, als
|
|
* da ist. */
|
|
export function bildAdresseErlaubt(roh) {
|
|
let u;
|
|
try { u = new URL(String(roh)); } catch { return false; }
|
|
const h = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
/* NUR IN EINER PRUEFUNG: dann liegt der nachgebaute Bilddienst
|
|
auf 127.0.0.1. Derselbe Schalter wie bei istTikTok -- eine
|
|
Pruefung, die die echte Regel umgeht, prueft eine andere. */
|
|
if (process.env.TIKTOK_KAMPAGNE_BASIS && (h === "127.0.0.1" || h === "localhost")) {
|
|
return true;
|
|
}
|
|
if (u.protocol !== "https:") return false;
|
|
if (u.port && u.port !== "443") return false;
|
|
if (PRIVATE_NAMEN.test(h)) return false;
|
|
if (PRIVATE_ZAHLEN.test(h)) return false;
|
|
return true;
|
|
}
|
|
|
|
/** Folgt einer Umleitung Station fuer Station.
|
|
*
|
|
* Gibt { ok: true, url, weg } oder { ok: false, grund, ... } mit
|
|
* grund aus: "fremd" (verlaesst TikTok), "zu_viele", "unlesbar",
|
|
* "frist", "netz".
|
|
*
|
|
* Der Koerper jeder Zwischenantwort wird WEGGEWORFEN, nicht gelesen:
|
|
* Sonst bleibt die Verbindung offen und wir laden jede Zwischenseite
|
|
* umsonst herunter. */
|
|
export async function folgeUmleitung(roh, { frist = FRIST_SEITE_MS,
|
|
schritte = UMLEITUNGEN_MAX } = {}) {
|
|
let hier = String(roh || "");
|
|
if (!istTikTok(hier)) {
|
|
return { ok: false, grund: "fremd", url: hier, host: hostVon(hier), weg: [] };
|
|
}
|
|
const weg = [hier];
|
|
/* EINE Frist fuer die GANZE Kette. Fuenf Spruenge mit je zwoelf
|
|
Sekunden waeren eine Minute -- und ein Mensch, der auf einen Knopf
|
|
gedrueckt hat, wartet keine Minute. */
|
|
const abbruch = new AbortController();
|
|
const uhr = setTimeout(() => abbruch.abort(), frist);
|
|
try {
|
|
for (let i = 0; i < schritte; i++) {
|
|
let a;
|
|
try {
|
|
a = await fetch(hier, {
|
|
method: "GET", redirect: "manual", signal: abbruch.signal,
|
|
headers: { "user-agent": KENNUNG },
|
|
});
|
|
} catch (f) {
|
|
return { ok: false, grund: abbruch.signal.aborted ? "frist" : "netz",
|
|
url: hier, host: hostVon(hier), weg, fehler: f?.message };
|
|
}
|
|
try { await a.body?.cancel(); } catch { /* egal */ }
|
|
const ort = a.headers.get("location");
|
|
if (!ort) return { ok: true, url: hier, weg, status: a.status };
|
|
let naechste;
|
|
try { naechste = new URL(ort, hier).toString(); }
|
|
catch { return { ok: false, grund: "unlesbar", url: ort, host: hostVon(ort), weg }; }
|
|
if (!istTikTok(naechste)) {
|
|
return { ok: false, grund: "fremd", url: naechste, host: hostVon(naechste), weg };
|
|
}
|
|
hier = naechste;
|
|
weg.push(hier);
|
|
}
|
|
return { ok: false, grund: "zu_viele", url: hier, host: hostVon(hier), weg };
|
|
} finally { clearTimeout(uhr); }
|
|
}
|
|
|
|
/** Holt eine ganze Seite -- mit Frist UND mit einer Grenze, die beim
|
|
* LESEN gezogen wird.
|
|
*
|
|
* Gibt { ok: true, status, text, bytes } oder { ok: false, grund }
|
|
* mit grund aus: "frist", "zu_gross", "netz", "status". Ein Aufrufer,
|
|
* der jeden dieser Faelle verschieden beantworten muss, soll sie
|
|
* nicht aus einer Fehlermeldung raten. */
|
|
export async function holeSeite(adresse, { frist = FRIST_SEITE_MS,
|
|
maxBytes = SEITE_MAX } = {}) {
|
|
const abbruch = new AbortController();
|
|
const uhr = setTimeout(() => abbruch.abort(), frist);
|
|
try {
|
|
let a;
|
|
try {
|
|
a = await fetch(adresse, {
|
|
signal: abbruch.signal, redirect: "error",
|
|
headers: { "user-agent": KENNUNG, accept: "text/html" },
|
|
});
|
|
} catch (f) {
|
|
return { ok: false, grund: abbruch.signal.aborted ? "frist" : "netz",
|
|
fehler: f?.message };
|
|
}
|
|
if (!a.ok) {
|
|
try { await a.body?.cancel(); } catch { /* egal */ }
|
|
return { ok: false, grund: "status", status: a.status };
|
|
}
|
|
/* Ein Kopf, der die Groesse schon ansagt, erspart das Herunterladen
|
|
-- aber er ist eine BEHAUPTUNG des fremden Servers und kann
|
|
fehlen oder luegen. Deshalb beides: hier nachsehen UND unten
|
|
beim Lesen mitzaehlen. */
|
|
const angesagt = Number(a.headers.get("content-length"));
|
|
if (Number.isFinite(angesagt) && angesagt > maxBytes) {
|
|
try { await a.body?.cancel(); } catch { /* egal */ }
|
|
return { ok: false, grund: "zu_gross", bytes: angesagt, angesagt: true };
|
|
}
|
|
const leser = a.body?.getReader?.();
|
|
if (!leser) return { ok: false, grund: "netz", fehler: "kein Koerper" };
|
|
const stuecke = [];
|
|
let gesamt = 0;
|
|
for (;;) {
|
|
let stueck;
|
|
try { stueck = await leser.read(); }
|
|
catch (f) {
|
|
return { ok: false, grund: abbruch.signal.aborted ? "frist" : "netz",
|
|
fehler: f?.message };
|
|
}
|
|
if (stueck.done) break;
|
|
gesamt += stueck.value.byteLength;
|
|
if (gesamt > maxBytes) {
|
|
try { await leser.cancel(); } catch { /* egal */ }
|
|
return { ok: false, grund: "zu_gross", bytes: gesamt, angesagt: false };
|
|
}
|
|
stuecke.push(Buffer.from(stueck.value));
|
|
}
|
|
return { ok: true, status: a.status, bytes: gesamt,
|
|
text: Buffer.concat(stuecke).toString("utf8") };
|
|
} finally { clearTimeout(uhr); }
|
|
}
|
|
|
|
/** Holt etwas von draussen -- MIT Frist ueber den ganzen Vorgang.
|
|
*
|
|
* DIE ALTE FASSUNG HAT NUR DEN ANFANG BEWACHT (behoben 05.10.2026).
|
|
*
|
|
* Sie gab die Antwort zurueck, sobald die Kopfzeilen da waren, und
|
|
* raeumte im `finally` die Uhr weg. Was danach kam -- `a.json()` beim
|
|
* Abruf der Videodaten, `a.arrayBuffer()` beim Vorschaubild -- lief
|
|
* OHNE jede Frist. Bleibt der fremde Server mitten im Koerper
|
|
* stehen, wartet unsere Anfrage fuer immer.
|
|
*
|
|
* Genau das sieht man als "der Knopf bleibt auf `wird geholt ...`
|
|
* stehen": keine Fehlermeldung, kein Eintrag, kein Protokolleintrag
|
|
* -- weil der Vorgang nie endet. Und es braucht keine Aenderung bei
|
|
* uns, um anzufangen: Es haengt am fremden Netz. "Es ging doch immer
|
|
* bis jetzt" ist bei so einem Fehler die Regel, nicht die Ausnahme.
|
|
*
|
|
* Der Koerper wird deshalb GELESEN, solange die Uhr laeuft, und erst
|
|
* danach abgeraeumt. Die Frist gilt damit fuer alles zusammen. */
|
|
export async function holeMitFrist(adresse, kopf = {}, { frist = FRIST_MS } = {}) {
|
|
const abbruch = new AbortController();
|
|
const uhr = setTimeout(() => abbruch.abort(), frist);
|
|
try {
|
|
const a = await fetch(adresse, { signal: abbruch.signal, headers: kopf, redirect: "follow" });
|
|
/* Bei einem Fehlschlag interessiert der Koerper nicht -- aber er
|
|
muss trotzdem weg, sonst bleibt die Verbindung offen. */
|
|
const roh = Buffer.from(await a.arrayBuffer());
|
|
return {
|
|
ok: a.ok, status: a.status, headers: a.headers, roh,
|
|
text: () => roh.toString("utf8"),
|
|
};
|
|
} finally { clearTimeout(uhr); }
|
|
}
|