So lösen Sie Captchas in einem Trigger.dev-Background-Task

trigger.dev captcha - How to Solve CAPTCHAs in a Trigger.dev Background Task

Eine Captcha-Lösung in Trigger.dev scheitert beim ersten Deploy aus einem Grund, der nichts mit dem Code zu tun hat. Trigger.dev ruft Ihre Anwendung nicht auf. Es baut Ihren Task in ein Docker-Image und führt ihn auf eigenen Maschinen aus, 127.0.0.1 innerhalb eines Tasks ist also dieser Container und nicht die Maschine, auf der Ihr Löser läuft. Der Server-Modus behebt das mit einer einzigen Einstellung. Das Zweite, was stimmen muss, ist maxDuration, denn das Pollen des Lösers wird vollständig auf dieses Budget angerechnet.

Was Sie brauchen

  • Ein Trigger.dev-Projekt mit installiertem SDK und einer trigger.config.ts im Wurzelverzeichnis.
  • CapSkip läuft auf einer Windows-Maschine, und der Node-Client ist im selben Projekt hinzugefügt, damit er im deployten Image landet.
  • Sitekey und Seiten-URL kommen über das Task-Payload herein, statt fest im Code zu stehen, damit ein einziger Task jedes Formular bedient.
  • Der Server-Modus, dazu eine erreichbare Adresse für den Löser. Auf Trigger.dev Cloud ist das nicht optional, aus dem in Schritt 1 genannten Grund.
# npm install capskip
npm install @trigger.dev/sdk capskip

Schritt 1: Wo der Task tatsächlich läuft, und welchen Modus das erfordert

Die meisten Plattform-Anleitungen können das ans Ende schieben. Diese hier nicht, denn davon hängt ab, ob überhaupt etwas anderes funktioniert. Trigger.dev beschreibt einen Deploy selbst ganz nüchtern: Der Code wird in ein Docker-Image gepackt und an Ihre Trigger.dev-Instanz ausgeliefert, und jeder Lauf wird in einer isolierten Umgebung ausgeführt, die Trigger.dev verwaltet. Ihr Task läuft nicht dort, wo Ihr Editor steht.

Loopback innerhalb der run-Funktion zeigt also auf den Task-Container. Dort lauscht nichts auf Port 8080, und der Fehler ist ein abgewiesener Verbindungsversuch, der bei jedem Anlauf als NetworkException auftaucht.

Es gibt zwei Verbindungsmodi. Der Local-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät, und das ist richtig, wenn Ihre Automatisierung und der Löser sich eine Maschine teilen. Der Server-Modus bindet an Ihre Netzwerkadresse oder öffentliche IP, sodass ein anderer Rechner, ein VPS oder eine gehostete Plattform dieselbe Windows-Maschine über die API erreichen kann. Der Server-Modus ändert nur, auf welcher Adresse der Löser lauscht. Es ist weiterhin Ihre Hardware, und es wird weiterhin nicht pro Lösung abgerechnet. Beide Modi finden Sie unter Verbindungseinstellungen.

Wie Sie Trigger.dev betreibenWelcher Verbindungsmodus
Die dev-CLI, auf der CapSkip-MaschineLocal-Modus. 127.0.0.1 ist hier wirklich richtig
Selbst gehostet, in Ihrem eigenen NetzwerkServer-Modus mit der LAN-Adresse des Solvers
Trigger.dev CloudServer-Modus mit einer statischen öffentlichen IP und einer Firewallregel

Für die dritte Zeile lohnt sich eine statische öffentliche IP, damit die Adresse unter einem laufenden Deployment nicht wegrutscht. Legen Sie die Adresse in eine Umgebungsvariable statt in den Quellcode, denn die dev-CLI und der deployte Task wollen unterschiedliche Werte.

// npm install capskip
import { CapSkip } from "capskip";

// 127.0.0.1 while the dev CLI runs it on your machine,
// the solver's reachable address once it is deployed.
export const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: Number(process.env.CAPSKIP_PORT ?? 8080),
  recaptchaTimeout: 120,
});

Schritt 2: maxDuration muss die Lösung abdecken

Trigger.dev misst einen Lauf in Sekunden gegen maxDuration, mit einem dokumentierten Minimum von fünf. Die Ausnahmen sind ausdrücklich benannt: Zeit in wait.for, triggerAndWait und batchTriggerAndWait zählt nicht mit. Ein awaiteter HTTP-Request steht nicht auf dieser Liste, also zählt jede Sekunde, die der Client mit dem Pollen des Lösers verbringt, in voller Höhe mit.

Das ist wichtig, weil Lösen zum größten Teil Warten ist. Eine reCAPTCHA-v2-Lösung dauert üblicherweise fünfzehn bis fünfundvierzig Sekunden, und eine ausgelastete Warteschlange kann das weiter nach oben treiben. Setzen Sie maxDuration so, dass die Lösung im Budget steckt, nicht nur der Rest des Tasks.

// trigger.config.ts sets the project-wide floor.
import { defineConfig } from "@trigger.dev/sdk";

export default defineConfig({
  project: "proj_YOUR_PROJECT_REF",
  maxDuration: 60,
});

// A solving task overrides it. 60s is not enough on its own:
// the solve alone can use most of that budget.
export const solveAndSubmit = task({
  id: "solve-and-submit",
  maxDuration: 300,
  run: async (payload) => { /* ... */ },
});

Halten Sie die eigene Obergrenze des Clients darunter, damit der Client zuerst aufgibt und etwas Lesbares wirft. Der Node-Client steht standardmäßig auf 300 Sekunden für reCAPTCHA, Turnstile und GeeTest und auf 120 für Bild-Captchas. Wenn Sie das Client-Timeout für reCAPTCHA auf 120 senken, bleibt unter einer maxDuration von 300 Raum für alles, was der Task mit dem Token noch anstellt.

Eine Falle verdient eine ausdrückliche Erwähnung. Weil wait.for von maxDuration ausgenommen ist, wirkt es wie eine kostenlose Möglichkeit, einen Task zu pausieren. Für ein Token ist es nicht kostenlos. Ein reCAPTCHA-Token ist rund zwei Minuten echter Zeit gültig, und die Uhr der Plattform und die Uhr des Tokens sind zwei verschiedene Uhren. Halten Sie Lösen und Einlösen im selben Codeabschnitt, und lesen Sie vorab einmal nach, wie lange ein reCAPTCHA-Token gültig ist und was das für Ihren Entwurf bedeutet.

Schritt 3: Wiederholungen, und welche Fehler eine verdienen

Tasks werden standardmäßig dreimal wiederholt, mit exponentiellem Backoff, den Sie über factor, minTimeoutInMs, maxTimeoutInMs und randomize konfigurieren. Die von der CLI erzeugte Konfiguration schaltet Wiederholungen in der DEV-Umgebung ab, und deshalb wirkt ein Task, der in der Produktion Wiederholungen macht, auf Ihrer Maschine wie ein sofortiger Fehlschlag.

Ein wiederholter Task führt die gesamte run-Funktion erneut aus und löst deshalb noch einmal. Es gibt kein abgestandenes Token zu erben, und das macht die Standardwerte vernünftig. Justieren sollten Sie eher, welche Fehler überhaupt einen Versuch bekommen.

Welche AusnahmeWas es bedeutetWiederholung sinnvoll?
NetworkExceptionCapSkip war nicht erreichbar oder startet gerade neuJa. Genau dafür sind Wiederholungen da
TimeoutExceptionDas Polling lief über die eigene Obergrenze des Clients hinausEinmal, vielleicht. Drei Versuche lohnen selten
ApiExceptionDie API hat einen Fehlercode zurückgegebenKommt auf den Fehlercode an. Meistens nein
ValidationExceptionDie Parameter waren falsch und werden es wieder seinNein. Werfen Sie AbortTaskRunError

AbortTaskRunError lässt den Versuch fehlschlagen und schaltet Wiederholungen ab, und genau das verdient eine fehlerhafte Anfrage. Ein falscher Sitekey wird auch beim dritten Anlauf nicht richtig, und drei Versuche zu je neunzig Sekunden sind viereinhalb Minuten, die allein dem Beweis dafür gewidmet sind.

import { task, AbortTaskRunError } from "@trigger.dev/sdk";
import { ValidationException } from "capskip";

try {
  const { code } = await solver.recaptcha(sitekey, pageUrl);
  return await postForm(pageUrl, code);
} catch (err) {
  // Wrong parameters will be wrong on all three attempts.
  if (err instanceof ValidationException) {
    throw new AbortTaskRunError(err.message);
  }
  throw err;   // everything else takes the normal backoff
}

Schritt 4: Der vollständige Task

Alles von oben in einer Datei. Der Client wird im Modul-Scope erzeugt, damit er einmal pro Container und nicht einmal pro Lauf gebaut wird, und er trägt keinen Zustand pro Lauf mit sich.

// npm install capskip
import { task, AbortTaskRunError } from "@trigger.dev/sdk";
import { CapSkip, ValidationException } from "capskip";

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: 8080,
  recaptchaTimeout: 120,
});

export const submitSignup = task({
  id: "submit-signup",
  maxDuration: 300,
  retry: { maxAttempts: 3, minTimeoutInMs: 2000 },
  queue: { concurrencyLimit: 10 },
  run: async (payload, { ctx }) => {
    const { sitekey, pageUrl, email } = payload;

    try {
      // Solve and submit together. The token is short lived.
      const { code } = await solver.recaptcha(sitekey, pageUrl);
      const res = await postSignup(pageUrl, email, code);
      return { status: res.status, runId: ctx.run.id };
    } catch (err) {
      if (err instanceof ValidationException) {
        throw new AbortTaskRunError(err.message);
      }
      throw err;
    }
  },
});

Dieser Aufruf ist reCAPTCHA v2. Die anderen Typen haben dieselbe Form: Setzen Sie invisible oder enterprise auf 1, oder version auf v3 mit einer action, oder rufen Sie stattdessen turnstile oder geetest auf. Die vollständige Schnittstelle dokumentiert die Node.js-Captcha-Solver-Seite.

Schritt 5: Parallelität, und wo die eigentliche Obergrenze liegt

Die Option queue begrenzt, wie viele Läufe eines Tasks gleichzeitig ausgeführt werden. Bei einem Löser mit Verbrauchsabrechnung ist diese Zahl in Wahrheit eine Kostenbremse, und genau deshalb setzen viele sie niedrig an. Hier ist sie eine Kapazitätsfrage zu einer einzigen Maschine, stellen Sie sie also auf das ein, was der Löser und die Zielseite vertragen, und nicht auf das, was Sie sich leisten können.

Zwei Dinge begrenzen sie wirklich: die Windows-Maschine, auf der CapSkip läuft, und wie schnell die Seite, an die Sie absenden, Anfragen annimmt, bevor sie Sie drosselt. Das Zweite ist meist die engere Grenze. Nichts wartet hinter einem Guthaben, und nichts fällt zum Monatsende aus.

export const submitSignup = task({
  id: "submit-signup",
  // Sized for the solver machine and the target site,
  // not for a credit balance.
  queue: { concurrencyLimit: 10 },
  maxDuration: 300,
  run: async (payload) => { /* ... */ },
});

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Funktioniert mit der dev-CLI, NetworkException nach dem DeployDer deployte Task ist ein Container, Loopback ist also der ContainerServer-Modus, und setzen Sie CAPSKIP_HOST für die deployte Umgebung
Der Lauf wird mitten in einer Lösung gestopptmaxDuration ist kürzer, als die Lösung dauertHeben Sie es am Task an, über das eigene Timeout des Clients
Ein Task scheitert in DEV sofort, wiederholt aber in der ProduktionDie erzeugte Konfiguration schaltet Wiederholungen in DEV abErwartetes Verhalten. Testen Sie Wiederholungen in einer deployten Umgebung
Drei Versuche, derselbe Fehler, mehrere Minuten verlorenEin Parameterfehler wird als vorübergehend behandeltWerfen Sie AbortTaskRunError bei einer ValidationException
Das Token wird nach einem wait.for abgelehntWarten ist für maxDuration kostenlos, für das Token nichtLösen Sie nach dem Warten, unmittelbar vor dem Absenden
ERROR_WRONG_USER_KEY in einer ApiExceptionCAPSKIP_API_KEY ist in der deployten Umgebung nicht gesetztSetzen Sie ihn in den Umgebungsvariablen von Trigger.dev und deployen Sie neu
CAPCHA_NOT_READY bei einer selbst gebauten Polling-SchleifeDas Ergebnis wurde gelesen, bevor es fertig warLassen Sie den Client pollen. Er drosselt von selbst

Diese letzte Antwort wird tatsächlich so geschrieben, und der fehlende Buchstabe ist kein Tippfehler auf unserer Seite, denn die API gibt sie wirklich so zurück. Ausführlich erklärt wird das im Leitfaden zu CAPCHA_NOT_READY.

FAQ

Kann ein Trigger.dev-Cloud-Task einen Löser auf meinem Schreibtisch erreichen?

Ja, mit dem Server-Modus. Der Task läuft in einem Container, den Trigger.dev verwaltet, Loopback ist dort also dieser Container. Binden Sie CapSkip unter den Verbindungseinstellungen an Ihre öffentliche IP, setzen Sie eine Firewallregel davor, die nur die erwarteten Adressen zulässt, und setzen Sie CAPSKIP_HOST in den Umgebungsvariablen von Trigger.dev. Eine statische öffentliche IP ist empfehlenswert, damit die Adresse Ihnen nicht wegrutscht.

Ändert ein selbst gehostetes Trigger.dev daran etwas?

Es ändert die Adresse, nicht das Modell. Auch selbst gehostete Läufe führen Ihren Code in Containern auf der Instanz aus, statt Ihre App aufzurufen, Loopback ist also weiterhin der Container. Der Unterschied ist, dass die Instanz meist in Ihrem eigenen Netzwerk steht, sodass der Server-Modus eine LAN-Adresse statt einer öffentlichen nutzen kann und keine Firewallregel zum Internet zeigen muss.

Sollte das Lösen ein eigener Task sein, den andere Tasks aufrufen?

Meistens nicht. Eine Aufteilung bedeutet, dass das Token eine Task-Grenze überquert und in einem Payload liegt, während der übergeordnete Task weiterläuft, und das ist der schnellste Weg, ein bereits abgelaufenes Token einzusetzen. Halten Sie das Lösen und alles, was das Token verbraucht, in derselben run-Funktion und geben Sie das Ergebnis zurück statt der Zugangsdaten. Ein eigener Task lohnt sich nur, wenn das, was er zurückgibt, gar kein Token ist.

Worin unterscheidet sich das von Inngest?

Das Deployment-Modell ist genau umgekehrt, und das ändert die gesamte Antwort. Inngest ruft Ihre Anwendung über HTTP auf, Ihr Code läuft also dort, wo Sie ihn deployt haben, und der Verbindungsmodus ist eine Frage Ihres eigenen Hostings. Trigger.dev führt Ihren Code auf eigenen Maschinen aus, im Cloud-Angebot ist der Server-Modus damit für Sie entschieden. Die Inngest-Variante, samt der Begründung, warum eine Lösung dort in einem einzigen Step liegen muss, finden Sie in der Inngest-Anleitung.

Die Kurzfassung

Betreiben Sie CapSkip im Server-Modus und setzen Sie CAPSKIP_HOST in der Trigger.dev-Umgebung, denn ein deployter Task ist ein Container, und Loopback ist dort dieser Container. Geben Sie dem Task eine maxDuration, die die Lösung abdeckt, denn das Pollen eines HTTP-Endpunkts gehört nicht zu den ausgenommenen Wartezeiten. Halten Sie das Timeout des Clients darunter. Lassen Sie die drei standardmäßigen Wiederholungen stehen, werfen Sie aber bei Parameterfehlern AbortTaskRunError. Lösen und Absenden gehören in dieselbe run-Funktion, niemals über ein Warten oder eine Task-Grenze hinweg.

Bevor Sie ein Parallelitätslimit festlegen, sollten Sie eines abwägen: CapSkip ist ein Captcha-Löser und läuft auf Hardware, die Sie ohnehin besitzen, sodass die Zahl, die Sie wählen, eine Kapazitätsentscheidung ist und keine Budgetfrage.