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" };