From b42bbb1d8805046dab38eeff913d633cecb56b45 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Mon, 5 Oct 2026 12:39:49 +0200 Subject: [PATCH] Video einlesen: die Frist galt nur fuer den Anfang -- und kein Fehlschlag stand im Protokoll Filipe: "ES GING DOCH IMMER BIS JETZT." Das war der nuetzlichste Satz des Tages, denn er stimmt: Zwischen seinem letzten Erfolg (04.10. 13:44) und der Meldung wurde NICHTS ausgeliefert -- gemessen am Server: ein einziger Neustart seit dem 04.10. 13:00, meiner von heute 12:19. Am Haus hat sich nichts geaendert. Ein Fehler, der ohne Aenderung anfaengt, haengt an etwas von draussen. Und genau dafuer war die Frist zu kurz gebaut: holeMitFrist() gab die Antwort zurueck, sobald die KOPFZEILEN da waren, und raeumte im finally die Uhr weg. Was danach kam -- a.json() fuer die Videodaten, a.arrayBuffer() fuer das Vorschaubild -- lief OHNE jede Frist. Bleibt TikToks Bildserver mitten im Koerper stehen, wartet unsere Anfrage fuer immer. Von aussen sieht das genau so aus, wie Filipe es beschrieben hat: "der Knopf bleibt auf `wird geholt ...` stehen" -- keine Meldung, kein Eintrag, kein Protokolleintrag, weil der Vorgang nie endet. Das Vorschaubild ist dabei kein Nebending: das letzte war 1,29 MB. Der Koerper wird jetzt gelesen, SOLANGE die Uhr laeuft. Acht Sekunden gelten fuer alles zusammen. NACHGEMESSEN, nicht vermutet: oembed antwortet dem Server in 0,2 s, das Bild laedt in 0,1 s -- heute haengt es also nicht. Der Fehler ist trotzdem echt und erklaert genau dieses Bild; er kommt und geht mit dem fremden Netz. UND DER ZWEITE TEIL, der die Suche so teuer gemacht hat: Im Protokoll stand nur der ERFOLG (video_eingelesen). Blieb TikTok eine Antwort schuldig, gab es hinterher nichts zum Nachsehen -- auf die Frage "ist der Versuch ueberhaupt angekommen?" liess sich nichts sagen. Dieselbe Reihenfolge wie am 06.09. im Shop ("erst fragen, OB gesendet wurde") und wie heute frueh beim Push. Jetzt steht jeder Fehlschlag als `video_gescheitert` im Protokoll, mit Grund: keine TikTok-Adresse, TikTok antwortet nicht, oder Abbruch mit Meldung. Gegengemessen: mess-teilen weiterhin 201 mit Eintrag und Vorschaubild, pruef-video 74, pruef-teilen 27, pruef-kanalzeile 14 -- alle 0 Fehler. Co-Authored-By: Claude Opus 5 --- server/workspace-video.js | 64 ++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/server/workspace-video.js b/server/workspace-video.js index c789eec5..96155788 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -94,11 +94,36 @@ export function istTikTok(roh) { } /** Mit Frist holen. Ohne sie haengt die Anfrage am fremden Dienst. */ +/** 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. 8 Sekunden gelten damit fuer alles zusammen. */ async function holeMitFrist(adresse, kopf = {}) { const abbruch = new AbortController(); const uhr = setTimeout(() => abbruch.abort(), FRIST_MS); try { - return await fetch(adresse, { signal: abbruch.signal, headers: kopf, redirect: "follow" }); + 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); } } @@ -120,7 +145,8 @@ export async function videoAuskunft(url) { `${OEMBED_BASIS}?url=${encodeURIComponent(url)}`, { accept: "application/json" }); if (!a.ok) return { fehler: `TikTok antwortet nicht (${a.status}).` }; - const j = await a.json().catch(() => null); + let j = null; + try { j = JSON.parse(a.text()); } catch { j = null; } if (!j || !j.title) return { fehler: "TikTok hat nichts zu diesem Video geschickt." }; /* Der Account steht in `author_url` (…/@name). `author_name` ist der ANZEIGENAME und aendert sich, wenn jemand ihn umstellt -- als @@ -141,9 +167,7 @@ async function coverAblegen(adresse) { let a; try { a = await holeMitFrist(adresse); } catch { return null; } if (!a.ok) return null; - const lang = Number(a.headers.get("content-length") || 0); - if (lang > COVER_MAX) return null; - const roh = Buffer.from(await a.arrayBuffer()); + const roh = a.roh; if (!roh.length || roh.length > COVER_MAX) return null; /* DIESELBE BYTEPRUEFUNG WIE BEI HOCHGELADENEN DATEIEN. Was von draussen kommt, ist von draussen -- egal ob jemand es hochlaedt @@ -177,6 +201,10 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { const url = String(req.body?.url ?? "").trim(); if (!istTikTok(url)) { + protokolliere("video_gescheitert", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `${bereich} ${String(url).slice(0, 120)} -- keine TikTok-Adresse`, + }); return res.status(400).json({ fehler: "Das ist keine TikTok-Adresse." }); } @@ -234,7 +262,20 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { } const auskunft = await videoAuskunft(url); - if (auskunft.fehler) return res.status(502).json({ fehler: auskunft.fehler }); + if (auskunft.fehler) { + /* NICHT STILL SCHEITERN. Bis heute stand im Protokoll nur der + Erfolg (`video_eingelesen`). Blieb TikTok eine Antwort + schuldig, gab es hinterher nichts zum Nachsehen -- und auf + die Frage "warum kam mein Video nicht an?" liess sich nicht + einmal sagen, OB der Versuch den Server erreicht hat. Genau + diese Reihenfolge hat am 06.09. im Shop zwei Stunden + gekostet, und heute noch einmal einen halben Tag. */ + protokolliere("video_gescheitert", { + personId: req.person.id, rolle: req.person.rolle, ip: echteIp(req), + detail: `${bereich} ${url} -- ${auskunft.fehler}`.slice(0, 180), + }); + return res.status(502).json({ fehler: auskunft.fehler }); + } /* NUR SEINE EIGENEN KANAELE. Ein fremdes Video in Filipes Highlights waere nicht bloss falsch einsortiert -- es waere ein @@ -344,6 +385,14 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { Datei ohne Eintrag findet niemand wieder und niemand loescht sie -- sie liegt einfach fuer immer da. */ if (abgelegt) { try { unlinkSync(join(ORDNER, abgelegt.name)); } catch { /* egal */ } } + /* Auch der Absturz gehoert ins Protokoll -- er ist der Fall, bei + dem sonst GAR nichts dasteht. */ + try { + protokolliere("video_gescheitert", { + personId: req.person?.id, rolle: req.person?.rolle, ip: echteIp(req), + detail: `Abbruch: ${String(fehler?.message || fehler)}`.slice(0, 180), + }); + } catch { /* das Protokoll darf die Antwort nicht aufhalten */ } console.error("[workspace] Video einlesen:", fehler?.message); res.status(503).json({ fehler: "nicht_verfuegbar" }); } @@ -429,7 +478,8 @@ export async function videoNochDa(url) { return { da: false, grund: `TikTok kennt es nicht (${a.status})` }; } if (!a.ok) return { da: null, grund: `unerwartet (${a.status})` }; - const j = await a.json().catch(() => null); + let j = null; + try { j = JSON.parse(a.text()); } catch { j = null; } /* Antwort ohne Titel: TikTok liefert das auch fuer entfernte Videos mit Status 200. Ein leerer Titel ist deshalb eine Aussage. */ if (!j || !j.title) return { da: false, grund: "Antwort ohne Titel" };