TikTok liefert die Kampagnenseite nicht mehr aus -- und der Kasten sagt es jetzt
Filipe hat die Kachel benutzt und bekam "Ging nicht." Im Protokoll des
Servers stand der Grund; im Kasten stand er nicht. Beides ist jetzt
behoben -- und beim Nachmessen kam ein groesserer Befund heraus.
DER BEFUND: DIE SEITE KOMMT LEER AN
Gemessen am 07.10.2026, von zwei Leitungen und auf fuenf Wegen:
ehrliche Kennung, accept text/html 17 KB, keine Daten
ehrliche Kennung + Browser-Accept 17 KB, keine Daten
Browser-Kennung 17 KB, keine Daten
Browser-Kennung + Accept + Sprache 17 KB, keine Daten
ganz ohne eigene Koepfe 17 KB, keine Daten
zweiter Abruf MIT Keksen 17 KB, keine Daten
echtes Chromium (headless) 17 KB, LEERE Seite
Titel "Campaign", leerer Koerper, zwei Skripte (tiktok-environment,
gfdatav1). Dasselbe fuer Filipes neue Kampagne UND fuer
"Gipfelstuermer", von meinem Rechner wie vom Server.
Damit traegt eine Annahme des Bauplans nicht: "HTML 1.100.298
Zeichen, serverseitig gerendert". Die 1,1-MB-Seiten, an denen der
Bauplan entwickelt wurde, stammen aus einer echten Browsersitzung.
Auf einen gewoehnlichen Abruf baut TikTok die Seite nicht mehr auf.
Ob das voruebergehend ist (Modern.js kann SSR abstufen -- die
gespeicherten Seiten tragen "renderLevel":2) oder bleibt, laesst
sich an einem Abend nicht sagen. Der Weg bleibt deshalb eingebaut:
Kommt die Seite wieder mit Daten, laeuft alles sofort.
WAS DER MENSCH DAVOR JETZT SIEHT
Der Leser unterscheidet HUELLE von UMBAU. Das sind zwei sehr
verschiedene Lagen: Bei einem Umbau ist etwas zu reparieren, bei
einer Huelle kann niemand etwas machen -- und soll das hoeren statt
zu suchen. Die Meldung sagt die Groesse, sagt "das liegt nicht an
dir und nicht am Link" und nennt den naechsten Schritt.
"umgebaut" steht dort ausdruecklich NICHT mehr: Dieser Satz haette
ihn auf die falsche Suche geschickt.
UND NIE WIEDER "Ging nicht."
Der Verlust der Meldung liess sich NICHT nachstellen -- 403 und 502
kommen beide woertlich im Kasten an, jetzt in bild-kampagne
gemessen (25 Messungen). Ein Ersatztext, der nichts sagt, ist aber
auch ohne bekannte Ursache falsch: Er laesst raten. Er nennt jetzt
die Nummer der Antwort und sagt, wo mehr steht.
GEPRUEFT
pruef-kampagne-lesen 108 -> 112 (Huelle und Umbau getrennt, mit
Gegenprobe: eine fremde Seite gilt NICHT als Huelle) ·
pruef-kampagne 99 -> 102 (fuenf verschiedene Saetze fuer fuenf
Faelle) · bild-kampagne 20 -> 25 (zeigt der Kasten den Grund?) ·
pruef-struktur, pruef-eventkarte 94, pruef-agentur 62,
pruef-css-klassen 39, pruef-zeichen 8, pruef-deutsche-texte 12
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -254,6 +254,89 @@ await messen(412, 915, "Auf dem Handy, haeufigste Android-Breite");
|
||||
`keine Bewegung bei prefers-reduced-motion (${ruht.animation}, ${ruht.dauer})`);
|
||||
}
|
||||
|
||||
/* ---- SAGT DER KASTEN, WARUM ES NICHT GING? -------------------------
|
||||
|
||||
Am 07.10.2026 stand bei Filipe im Kasten "Ging nicht." -- der
|
||||
Ersatztext. Im Protokoll des Servers stand dagegen der genaue
|
||||
Grund. Zwischen beiden liegt die ganze Frage, ob jemand weiss, was
|
||||
zu tun ist.
|
||||
|
||||
GEMESSEN WIRD HIER DER ECHTE WEG: Auf einer Pruefadresse hat die
|
||||
Sitzung kein Haus, die Route lehnt deshalb mit 403 und einem
|
||||
ganzen deutschen Satz ab. Genau dieser Satz muss im Kasten
|
||||
stehen -- nicht "Ging nicht.".
|
||||
|
||||
Das ist kein Umweg: Jede Fehlermeldung dieser Route geht
|
||||
denselben Weg (res.status(...).json({ fehler })). Wer hier
|
||||
ankommt, kommt ueberall an. */
|
||||
{
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const seite = await ctx.newPage();
|
||||
await anmelden(seite, "admin", "CODE-DOGI-0001");
|
||||
await seite.goto(BASIS + "/workspace/bereich.html?b=agentur", { waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(1400);
|
||||
|
||||
await seite.fill("#kampagne-text",
|
||||
"Dein Creator Network lädt dich ein: https://www.tiktok.com/t/ZSbqPsFa8/");
|
||||
await seite.$eval("#kampagne-holen", (b) => b.click());
|
||||
/* Auf eine ANTWORT warten, nicht auf eine feste Zeit: Ein fester
|
||||
Wert waere hier dieselbe Rechnung von gestern wie ueberall. */
|
||||
await seite.waitForFunction(
|
||||
() => document.getElementById("kampagne-sagt").textContent.trim() !== "Wird geholt …",
|
||||
{ timeout: 25000 }).catch(() => {});
|
||||
|
||||
const gezeigt = await seite.evaluate(() => ({
|
||||
text: document.getElementById("kampagne-sagt").textContent.trim(),
|
||||
knopfFrei: !document.getElementById("kampagne-holen").disabled,
|
||||
}));
|
||||
console.log("\n=== Was der Kasten nach einem Fehlschlag sagt ===");
|
||||
console.log(" gezeigt: " + JSON.stringify(gezeigt.text));
|
||||
|
||||
ok(gezeigt.text !== "Ging nicht.",
|
||||
"der Kasten zeigt NICHT den Ersatztext");
|
||||
ok(gezeigt.text.length > 25 && /[.!]$/.test(gezeigt.text),
|
||||
`sondern einen ganzen Satz (${gezeigt.text.length} Zeichen)`);
|
||||
ok(/Agentur|workspace\.dogfather/i.test(gezeigt.text),
|
||||
"der den Grund nennt und sagt, was zu tun ist");
|
||||
ok(gezeigt.knopfFrei, "und der Knopf ist wieder frei");
|
||||
|
||||
/* UND DER FALL, DER FILIPE GETROFFEN HAT: HTTP 502.
|
||||
|
||||
Der 403 oben beweist, dass der Weg grundsaetzlich traegt. Die
|
||||
Meldung, die bei ihm verschwand, war aber ein 502 -- und ein
|
||||
502 kommt in diesem Haus durch einen anderen Flaschenhals
|
||||
(Vorbau, Dienst). Deshalb wird er hier EINZELN nachgestellt:
|
||||
window.holen gibt eine fertige 502-Antwort zurueck, genau so,
|
||||
wie der Server sie schickt. Zeigt der Kasten dann den Satz,
|
||||
liegt es nicht am Browsercode.
|
||||
|
||||
Nachgestellt wird die ANTWORT, nicht der Fehler -- damit misst
|
||||
diese Zeile genau eine Sache und nicht nebenbei TikTok. */
|
||||
await seite.evaluate(() => {
|
||||
const echt = window.holen;
|
||||
window.holen = async (pfad, o) => {
|
||||
if (!String(pfad).includes("/kampagne/einlesen")) return echt(pfad, o);
|
||||
return new Response(JSON.stringify({
|
||||
fehler: "Die Seite konnte nicht gelesen werden: "
|
||||
+ "__MODERN_ROUTER_DATA__ fehlt in der Seite. "
|
||||
+ "Vermutlich hat TikTok die Seite umgebaut.",
|
||||
}), { status: 502, headers: { "Content-Type": "application/json" } });
|
||||
};
|
||||
});
|
||||
await seite.fill("#kampagne-text", "https://www.tiktok.com/t/ZSbqPsFa8/");
|
||||
await seite.$eval("#kampagne-holen", (b) => b.click());
|
||||
await seite.waitForFunction(
|
||||
() => document.getElementById("kampagne-sagt").textContent.trim() !== "Wird geholt …",
|
||||
{ timeout: 20000 }).catch(() => {});
|
||||
const beiFuenfNullZwei = (await seite.evaluate(
|
||||
() => document.getElementById("kampagne-sagt").textContent.trim()));
|
||||
console.log(" bei HTTP 502: " + JSON.stringify(beiFuenfNullZwei));
|
||||
ok(beiFuenfNullZwei !== "Ging nicht."
|
||||
&& /MODERN_ROUTER_DATA/.test(beiFuenfNullZwei),
|
||||
"auch ein 502 zeigt den Satz des Servers, nicht den Ersatztext");
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
/* ---- Und ein Creator sieht den Kasten gar nicht --------------------- */
|
||||
{
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
|
||||
@@ -166,8 +166,33 @@ export function datenAusSeite(html) {
|
||||
const text = String(html || "");
|
||||
const treffer = DATENBLOCK.exec(text);
|
||||
if (!treffer) {
|
||||
/* ZWEI SEHR VERSCHIEDENE FAELLE, EINE FEHLENDE MARKE
|
||||
(nachgetragen 07.10.2026, nachdem genau das passiert ist).
|
||||
|
||||
(a) TikTok hat die Seite umgebaut -- dann kommt eine volle
|
||||
Seite an, nur anders gebaut. Das muss jemand ansehen.
|
||||
(b) TikTok schickt nur eine HUELLE, die sich erst im Browser
|
||||
aufbaut. Gemessen am 07.10.2026: 17 KB statt rund 1 MB,
|
||||
Titel "Campaign", leerer Koerper, zwei Skripte
|
||||
(tiktok-environment, gfdatav1). Von jedem Abrufweg aus
|
||||
dasselbe -- mit ehrlicher Kennung, mit Browser-Kennung,
|
||||
mit Keksen, sogar aus einem echten Chromium.
|
||||
|
||||
Fuer den Menschen davor ist der Unterschied alles: Bei (a)
|
||||
ist etwas zu reparieren, bei (b) kann er gar nichts machen
|
||||
und soll das auch hoeren. Eine Meldung, die beides
|
||||
zusammenwirft, schickt ihn auf die falsche Suche. */
|
||||
const huelle = text.length < 200_000
|
||||
&& (text.includes("gfdatav1") || text.includes("tiktok-environment"));
|
||||
throw new KampagneUnlesbar(
|
||||
"__MODERN_ROUTER_DATA__ fehlt in der Seite", "aufbau");
|
||||
huelle
|
||||
? `TikTok hat nur eine leere Hülle geschickt `
|
||||
+ `(${text.length < 1024 ? text.length + " Bytes"
|
||||
: Math.round(text.length / 1024) + " KB"} statt rund 1 MB). `
|
||||
+ `Die Seite baut sich erst im Browser auf -- daran kann `
|
||||
+ `von hier aus niemand etwas ändern`
|
||||
: "__MODERN_ROUTER_DATA__ fehlt in der Seite",
|
||||
"aufbau");
|
||||
}
|
||||
const anfang = treffer.index + treffer[0].length;
|
||||
const ende = text.indexOf("</script>", anfang);
|
||||
|
||||
@@ -542,6 +542,40 @@ function faengt(tun) {
|
||||
ok(faengt(() => leseKampagne(seite.gipfel)) === null,
|
||||
"Gegenprobe: die vollständige Seite wirft nicht");
|
||||
|
||||
/* DIE LEERE HUELLE IST ETWAS ANDERES ALS EIN UMBAU
|
||||
(nachgetragen 07.10.2026, nachdem genau das passiert ist).
|
||||
|
||||
Gemessen an diesem Tag: TikTok schickt auf einen gewoehnlichen
|
||||
Abruf nur noch 17 KB -- Titel "Campaign", leerer Koerper, zwei
|
||||
Skripte. Von jedem Weg aus dasselbe: ehrliche Kennung,
|
||||
Browser-Kennung, mit Keksen, sogar aus einem echten Chromium.
|
||||
Die Seite baut sich erst im Browser auf.
|
||||
|
||||
Fuer den Menschen davor ist der Unterschied alles: Bei einem
|
||||
Umbau ist etwas zu reparieren, bei einer Huelle kann er nichts
|
||||
machen -- und soll das hoeren statt zu suchen. */
|
||||
{
|
||||
const huelle = [
|
||||
"<!doctype html><html><head><meta charset=\"utf-8\">",
|
||||
"<title>Campaign</title>",
|
||||
'<script id="tiktok-environment" type="application/json">{"regionInfo":{}}</script>',
|
||||
'<script id="gfdatav1" type="application/json">{"env":"prod"}</script>',
|
||||
"</head><body><div id=\"root\"></div></body></html>",
|
||||
].join("");
|
||||
const fh = faengt(() => leseKampagne(huelle));
|
||||
ok(fh?.art === "aufbau", `eine Hülle wirft ebenfalls (${fh?.art})`);
|
||||
ok(/Hülle/.test(fh?.grund || "") && /(KB|Bytes)/.test(fh?.grund || ""),
|
||||
`und nennt sie beim Namen: „${fh?.grund}“`);
|
||||
ok(!/__MODERN_ROUTER_DATA__/.test(fh?.grund || ""),
|
||||
" ohne mit einer Marke um sich zu werfen, die hier niemandem hilft");
|
||||
|
||||
/* GEGENPROBE: Eine Seite OHNE TikToks Kennzeichen bleibt beim
|
||||
alten Satz -- sonst hiesse jeder Unsinn "Hülle". */
|
||||
const fremd = faengt(() => leseKampagne("<html><body>nichts</body></html>"));
|
||||
ok(/__MODERN_ROUTER_DATA__/.test(fremd?.grund || ""),
|
||||
"Gegenprobe: irgendeine fremde Seite gilt nicht als Hülle");
|
||||
}
|
||||
|
||||
/* Die kaputte Seite enthält bewusst ein ANDERES JSON-Skript und eine
|
||||
Zeile mit der Kampagnennummer. Wer „das erste Skript“ nimmt oder
|
||||
„irgendwo die Nummer“, wird hier grün und im Betrieb falsch. */
|
||||
|
||||
@@ -158,6 +158,18 @@ const SEITEN = {
|
||||
const ROH_GIPFEL = seiteLesen("kampagne-gipfelstuermer.html.br");
|
||||
const SEITE_KAPUTT = seiteLesen("kampagne-kaputt.html");
|
||||
|
||||
/* DIE LEERE HUELLE, wie TikTok sie am 07.10.2026 wirklich schickte:
|
||||
17 KB, Titel "Campaign", leerer Koerper, die Skripte
|
||||
tiktok-environment und gfdatav1. Hier nur so gross wie noetig --
|
||||
gemessen wird die Unterscheidung, nicht die Dateigroesse. */
|
||||
const HUELLE = [
|
||||
"<!doctype html><html><head><meta charset=\"utf-8\">",
|
||||
"<title>Campaign</title>",
|
||||
'<script id="tiktok-environment" type="application/json">{"regionInfo":{}}</script>',
|
||||
'<script id="gfdatav1" type="application/json">{"env":"prod"}</script>',
|
||||
"</head><body><div id=\"root\"></div></body></html>",
|
||||
].join("");
|
||||
|
||||
/* Eine Kampagne, deren Banner eine TEXTDATEI ist. Für die Gegenprobe
|
||||
zu `bildArtVon`: Was von draußen kommt, ist von draußen -- und der
|
||||
fremde Server behauptet den Typ. */
|
||||
@@ -199,6 +211,7 @@ const tikDienst = createServer((anfrage, antwort) => {
|
||||
if (u.pathname === "/t/fremd") return leite("https://beispiel.de/ganz-woanders");
|
||||
if (u.pathname === "/t/kreis") return leite("/t/kreis");
|
||||
if (u.pathname === "/t/kaputt") return leite("/falcon/kaputt");
|
||||
if (u.pathname === "/t/huelle") return leite("/falcon/huelle");
|
||||
if (u.pathname === "/t/ohneende") return leite("/falcon/ohneende");
|
||||
if (u.pathname === "/t/stumm") return leite("/falcon/stumm");
|
||||
if (u.pathname === "/t/riesig") return leite("/falcon/riesig");
|
||||
@@ -237,6 +250,12 @@ const tikDienst = createServer((anfrage, antwort) => {
|
||||
antwort.writeHead(200, { "content-type": "text/html" });
|
||||
return antwort.end(SEITE_KAPUTT);
|
||||
}
|
||||
if (u.pathname === "/falcon/huelle") {
|
||||
/* Genau das, was TikTok am 07.10.2026 wirklich schickte:
|
||||
eine kleine Seite mit Titel und zwei Skripten, ohne Inhalt. */
|
||||
antwort.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
||||
return antwort.end(HUELLE);
|
||||
}
|
||||
if (u.pathname === "/falcon/ohneende") {
|
||||
antwort.writeHead(200, { "content-type": "text/html" });
|
||||
return antwort.end(SEITE_OHNE_ENDE);
|
||||
@@ -675,6 +694,14 @@ melde("=== 9. Was schiefgehen kann, wird verständlich gesagt ===");
|
||||
ok(fremd.code === 400 && /beispiel\.de/.test(fremd.j.fehler || ""),
|
||||
`fremdes Ziel: HTTP ${fremd.code} — „${fremd.j.fehler}“`);
|
||||
|
||||
const huelle = await einlesen(`Klick: ${TIK}/t/huelle`);
|
||||
ok(huelle.code === 502 && /Hülle/.test(huelle.j.fehler || ""),
|
||||
`leere Hülle: HTTP ${huelle.code} — „${(huelle.j.fehler || "").slice(0, 95)}…“`);
|
||||
ok(/nicht an dir/.test(huelle.j.fehler || ""),
|
||||
" und sagt ausdrücklich, dass es nicht am Menschen davor liegt");
|
||||
ok(!/umgebaut/.test(huelle.j.fehler || ""),
|
||||
" ohne ihn auf die falsche Suche zu schicken");
|
||||
|
||||
const kaputt = await einlesen(`Klick: ${TIK}/t/kaputt`);
|
||||
ok(kaputt.code === 502 && /__MODERN_ROUTER_DATA__/.test(kaputt.j.fehler || ""),
|
||||
`umgebaute Seite: HTTP ${kaputt.code} — „${(kaputt.j.fehler || "").slice(0, 90)}…“`);
|
||||
@@ -687,8 +714,8 @@ melde("=== 9. Was schiefgehen kann, wird verständlich gesagt ===");
|
||||
nur „es kam ein Fehler“ sagt, wäre auch grün, wenn überall
|
||||
dasselbe stünde. */
|
||||
const saetze = new Set([ohneLink.j.fehler, fremd.j.fehler,
|
||||
kaputt.j.fehler, ohneEnde.j.fehler]);
|
||||
ok(saetze.size === 4, `${saetze.size} verschiedene Sätze für vier Fälle`);
|
||||
kaputt.j.fehler, ohneEnde.j.fehler, huelle.j.fehler]);
|
||||
ok(saetze.size === 5, `${saetze.size} verschiedene Sätze für fünf Fälle`);
|
||||
ok([...saetze].every((s) => s && !/\b50\d\b/.test(s) && s.length > 25),
|
||||
"keiner davon sagt „Fehler 502“, alle sind ganze Sätze");
|
||||
|
||||
|
||||
@@ -404,10 +404,16 @@ kampagneRouter.post("/workspace/api/kampagne/einlesen", async (req, res) => {
|
||||
beide mit 502 und beide mit dem GRUND im Klartext. Eine
|
||||
Kachel ohne Zeitraum wäre eine Falschauskunft; sie behauptet
|
||||
einen Termin, den sie nicht kennt. */
|
||||
/* DREI SAETZE STATT ZWEI (07.10.2026). Die leere Huelle
|
||||
ist kein Umbau und kein Fehler des Menschen davor -- und
|
||||
"vermutlich hat TikTok umgebaut" schickt ihn suchen. */
|
||||
const satz = fehler.art === "pflicht"
|
||||
? `Die Seite wurde gelesen, aber ${fehler.grund}. Bitte von Hand anlegen.`
|
||||
: `Die Seite konnte nicht gelesen werden: ${fehler.grund}. `
|
||||
+ "Vermutlich hat TikTok die Seite umgebaut.";
|
||||
: (/Hülle/.test(fehler.grund)
|
||||
? `${fehler.grund}. Das liegt nicht an dir und nicht am Link. `
|
||||
+ "Bitte die Kachel vorerst von Hand anlegen."
|
||||
: `Die Seite konnte nicht gelesen werden: ${fehler.grund}. `
|
||||
+ "Vermutlich hat TikTok die Seite umgebaut.");
|
||||
return schiefgegangen(502, satz, `${fehler.art}: ${fehler.grund}`);
|
||||
}
|
||||
throw fehler;
|
||||
|
||||
Reference in New Issue
Block a user