Tutorials

CAPTCHA-Handhabung in Progressive Web Apps (PWAs)

In einer Progressive Web App steht das CAPTCHA nicht im ersten HTML – es wird erst per JavaScript ins DOM nachgeladen, oft Sekunden nach dem Seitenaufruf. Genau daran scheitern die meisten Automatisierungen: Sie suchen nach dem Sitekey, bevor das Widget existiert. Die Lösung: aktiv auf das dynamische Rendering warten, Sitekey und Page-URL zum richtigen Zeitpunkt auslesen und das Token über CaptchaAI lösen.

Dieser Leitfaden zeigt Erkennung, Extraktion und Lösung von reCAPTCHA v2 und Cloudflare Turnstile in PWAs – mit Playwright und alternativ mit Selenium in Python.

Warum sich CAPTCHAs in PWAs anders verhalten

Klassische Websites liefern das CAPTCHA direkt in der ersten HTML-Antwort. Eine PWA nicht – diese Unterschiede sind die Ursache für abbrechende Skripte:

Aspekt Klassische Website PWA
CAPTCHA-Auslieferung im initialen HTML von JavaScript nach dem Laden gerendert
Navigation vollständiger Page-Reload clientseitiges Routing (kein Reload)
Service Worker nicht vorhanden cacht Ressourcen und kann Anfragen abfangen
DOM-Verfügbarkeit sofort erst nach dem Framework-Rendering
Netzwerkanfragen direkt können vom Service Worker abgefangen werden

PWA-Problem oder CAPTCHA-Problem? Schnelle Diagnose

Klären Sie zuerst, ob der Solver das Problem ist – oder der Zeitpunkt, zu dem Ihr Skript zugreift:

Beobachtung Wahrscheinlichste Ursache
Im initialen HTML kein CAPTCHA, später aber schon Das Widget wird erst clientseitig gerendert
Nach einem In-App-Wechsel erscheint plötzlich ein CAPTCHA Routing läuft ohne echten Page-Reload
Die Sitekey-Extraktion klappt nur sporadisch Der Auslesezeitpunkt passt nicht zum Framework-Lifecycle

Praxisbeispiel: Turnstile in einem Shopware-PWA-Storefront

Ein Szenario aus dem DACH-Raum: Ein Shopware-6-Shop wird als PWA-Storefront ausgeliefert und schützt den Login mit Cloudflare Turnstile. Beim ersten Rendern der App-Shell ist das Widget noch nicht im DOM – ein Skript, das sofort nach .cf-turnstile sucht, bricht ab. Erst nach dem Mounten der Frontend-Komponente erscheint der Container mit dem data-sitekey. Wer auf das Element wartet statt auf den load-Event, löst das Problem in einer Zeile – dasselbe Muster gilt für JTL-Frontends und für Termin- und Behördenportale.

Für ein stabiles Skript in diesem Setup kommt es auf drei Punkte an:

  • auf das gemountete Widget warten, nicht auf den load-Event des Dokuments
  • data-sitekey und Page-URL erst nach dem Rendern der Komponente auslesen
  • nach jedem clientseitigen Routenwechsel erneut auf ein CAPTCHA prüfen

Hinweis zur Sorgfaltspflicht: Beim automatisierten Abfragen fallen IP-Adressen an – nach DSGVO personenbezogene Daten. Prüfen Sie Rechtsgrundlage und Datenflüsse, bevor Sie produktiv gehen.

Schritt 1: Auf das dynamische CAPTCHA-Rendering warten

Der häufigste Fehler: den Sitekey auslesen, bevor das Framework das Widget gerendert hat. Warten Sie aktiv auf das Element – per MutationObserver oder Framework-Signal:

// pwa_captcha_detector.js — Playwright script
const { chromium } = require('playwright');
const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function detectCaptchaInPWA(page) {
  // Wait for the PWA app shell to render
  await page.waitForLoadState('networkidle');

  // Use MutationObserver to detect dynamically loaded CAPTCHAs
  const captchaInfo = await page.evaluate(() => {
    return new Promise((resolve) => {
      // Check if CAPTCHA is already present
      const existing = document.querySelector('.g-recaptcha, .cf-turnstile');
      if (existing) {
        resolve({
          type: existing.classList.contains('g-recaptcha')
            ? 'recaptcha_v2' : 'turnstile',
          sitekey: existing.getAttribute('data-sitekey'),
          pageurl: window.location.href,
        });
        return;
      }

      // Watch for CAPTCHA elements added dynamically
      const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node.nodeType !== 1) continue;
            const captcha = node.matches?.('.g-recaptcha, .cf-turnstile')
              ? node
              : node.querySelector?.('.g-recaptcha, .cf-turnstile');
            if (captcha) {
              observer.disconnect();
              resolve({
                type: captcha.classList.contains('g-recaptcha')
                  ? 'recaptcha_v2' : 'turnstile',
                sitekey: captcha.getAttribute('data-sitekey'),
                pageurl: window.location.href,
              });
              return;
            }
          }
        }
      });

      observer.observe(document.body, {
        childList: true,
        subtree: true,
      });

      // Timeout after 15 seconds
      setTimeout(() => {
        observer.disconnect();
        resolve(null);
      }, 15000);
    });
  });

  return captchaInfo;
}

async function main() {
  const browser = await chromium.launch({ headless: false });
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto('https://example-pwa.com/login');

  const captcha = await detectCaptchaInPWA(page);

  if (!captcha) {
    console.log('No CAPTCHA detected');
    await browser.close();
    return;
  }

  console.log(`Detected ${captcha.type}: ${captcha.sitekey}`);

  // Mit CaptchaAI lösen
  const token = await solveCaptcha(captcha);
  console.log(`Token: ${token.substring(0, 50)}...`);

  // Inject token
  await injectToken(page, captcha.type, token);

  // Submit form
  await page.click('button[type="submit"]');
  await page.waitForNavigation({ waitUntil: 'networkidle' });

  console.log('Form submitted');
  await browser.close();
}

async function solveCaptcha(captcha) {
  const params = {
    key: API_KEY,
    pageurl: captcha.pageurl,
    json: '1',
  };

  if (captcha.type === 'recaptcha_v2') {
    params.method = 'userrecaptcha';
    params.googlekey = captcha.sitekey;
  } else {
    params.method = 'turnstile';
    params.sitekey = captcha.sitekey;
  }

  const submit = await axios.get(
    'https://ocr.captchaai.com/in.php', { params }
  );
  if (submit.data.status !== 1) throw new Error(submit.data.request);

  const taskId = submit.data.request;

  for (let i = 0; i < 30; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: '1' },
    });
    if (poll.data.status === 1) return poll.data.request;
    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      throw new Error(poll.data.request);
    }
  }
  throw new Error('Timeout');
}

async function injectToken(page, type, token) {
  if (type === 'recaptcha_v2') {
    await page.evaluate((t) => {
      document.getElementById('g-recaptcha-response').value = t;
      try {
        const clients = ___grecaptcha_cfg.clients;
        Object.keys(clients).forEach((k) => {
          Object.keys(clients[k]).forEach((j) => {
            if (clients[k][j]?.callback) clients[k][j].callback(t);
          });
        });
      } catch (e) {}
    }, token);
  } else {
    await page.evaluate((t) => {
      const input = document.querySelector('[name="cf-turnstile-response"]');
      if (input) input.value = t;
      const cb = document.querySelector('.cf-turnstile')
        ?.getAttribute('data-callback');
      if (cb && typeof window[cb] === 'function') window[cb](t);
    }, token);
  }
}

main().catch(console.error);

Schritt 2: Veralteten Service-Worker-Cache behandeln

Service Worker cachen CAPTCHA-Skripte mitunter aggressiv – das führt zu veralteten Widgets. Umgehen Sie den Cache gezielt für die betroffenen Ressourcen:

// Intercept and bypass Service Worker cache for CAPTCHA scripts
await page.route('**/recaptcha/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

await page.route('**/turnstile/**', (route) => {
  route.continue({ headers: { ...route.request().headers(), 'Cache-Control': 'no-cache' } });
});

Schritt 3: Clientseitiges Routing überwachen

PWAs navigieren clientseitig – der Aufruf einer geschützten Route löst keinen klassischen Seitenladevorgang aus. Überwachen Sie Routenwechsel und prüfen danach erneut auf ein CAPTCHA:

// Monitor PWA route changes for new CAPTCHAs
await page.evaluate(() => {
  const originalPushState = history.pushState;
  history.pushState = function() {
    originalPushState.apply(this, arguments);
    window.dispatchEvent(new Event('pwa-route-change'));
  };
});

page.on('console', async (msg) => {
  // React to route changes if needed
});

// Or wait for specific route
await page.waitForURL('**/checkout', { waitUntil: 'networkidle' });
// Then detect CAPTCHA on the new route
const captcha = await detectCaptchaInPWA(page);

Schritt 4: Python-Alternative mit Selenium

Dieselbe Logik funktioniert in Python mit Selenium – praktisch, wenn Ihre Test-Pipeline ohnehin darauf aufsetzt. Entscheidend bleibt das explizite Warten auf das Widget:

# pwa_captcha_selenium.py
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

API_KEY = "YOUR_API_KEY"

driver = webdriver.Chrome()
driver.get("https://example-pwa.com/login")

# Wait for PWA to render CAPTCHA
wait = WebDriverWait(driver, 20)
captcha_el = wait.until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".g-recaptcha, .cf-turnstile"))
)

sitekey = captcha_el.get_attribute("data-sitekey")
pageurl = driver.current_url
is_turnstile = "cf-turnstile" in captcha_el.get_attribute("class")

# Submit to CaptchaAI
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if is_turnstile:
    params["method"] = "turnstile"
    params["sitekey"] = sitekey
else:
    params["method"] = "userrecaptcha"
    params["googlekey"] = sitekey

resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
task_id = resp.json()["request"]

# Poll
for _ in range(30):
    time.sleep(5)
    poll = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": "1",
    })
    if poll.json().get("status") == 1:
        token = poll.json()["request"]
        break
else:
    raise TimeoutError("CAPTCHA not solved")

# Inject token
driver.execute_script(f"""
    document.getElementById('g-recaptcha-response').value = '{token}';
""")

driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
print("Form submitted")
driver.quit()

Fehlerbehebung

Problem Ursache Lösung
CAPTCHA-Element taucht nie auf Route noch nicht fertig gerendert waitForSelector mit großzügigem Timeout; Routing muss abgeschlossen sein
Sitekey nach Navigation veraltet Service Worker liefert gecachtes HTML Cache-Header für CAPTCHA-Ressourcen umgehen (Schritt 2)
Token erzeugt, aber vom Ziel abgelehnt Sitekey, Page-URL oder Session passen nicht Parameter neu erfassen, Token in derselben Sitzung nutzen
Formular sendet Token nicht mit SPA-Handler liest aus dem Komponenten-State, nicht aus dem DOM Zusätzlich den Framework-State setzen (React-Ref, reaktive Vue-Property)

Häufige Fragen

Warum wird der Sitekey in meiner PWA nur sporadisch gefunden?

Weil das Widget zum Auslesezeitpunkt noch nicht im DOM liegt. Das Framework rendert es erst nach dem Mounten der Komponente. Warten Sie mit waitForSelector oder einem MutationObserver aktiv auf das Element – nicht auf DOMContentLoaded und nie auf ein festes sleep.

Wie erkenne ich in einer Single-Page-App den Wechsel zu einer CAPTCHA-Route?

Über die History-API. Da clientseitiges Routing keinen echten Seitenaufruf auslöst, hängen Sie sich an history.pushState oder nutzen page.waitForURL(...) und starten die Erkennung nach jedem Routenwechsel neu.

Kann ich in einer PWA mehrere CAPTCHAs gleichzeitig lösen?

Ja, begrenzt durch die Threads Ihres Plans. CaptchaAI rechnet Thread-basiert mit unbegrenzten Lösungen pro Thread – BASIC (15 $/Monat, 5 Threads) erlaubt fünf gleichzeitige Lösungen, ADVANCE (90 $/Monat, 50 Threads) entsprechend mehr.

Playwright oder Selenium – was passt besser für PWAs?

Beide funktionieren mit demselben Erkennungsmuster. Playwright bringt mit waitForURL und dem Request-Routing etwas mehr Komfort für clientseitiges Routing mit. Selenium ist die pragmatische Wahl, wenn Ihre Test-Pipeline ohnehin darauf aufsetzt – entscheidend bleibt in beiden Fällen das explizite Warten auf das Widget.

Reicht ein festes Sleep, um auf das Widget zu warten?

Nein. Ein festes sleep ist entweder zu kurz – dann fehlt das Widget noch – oder unnötig langsam. Warten Sie ereignisbasiert mit waitForSelector oder einem MutationObserver auf das konkrete Element; das ist schneller und deutlich stabiler.

Fazit

CAPTCHAs in PWAs sind kein grundsätzlich anderes Problem – nur ein Timing-Problem. Wer aktiv auf das gerenderte Widget wartet, Sitekey und Page-URL zum richtigen Zeitpunkt ausliest und clientseitige Routenwechsel überwacht, löst reCAPTCHA v2 und Turnstile in einer PWA genauso zuverlässig wie auf einer klassischen Seite. Die eigentliche Lösung übernimmt anschließend die CaptchaAI-API.


Verwandte Leitfäden

Kommentare sind für diesen Artikel deaktiviert.