Captchas in Inngest lösen, ohne zweimal zu lösen

inngest captcha - How to Solve CAPTCHAs in Inngest Without Solving Twice

Ein Captcha-Lösen in Inngest muss innerhalb eines einzigen step.run-Aufrufs stattfinden, und der Token muss innerhalb desselben Aufrufs verwendet werden. Inngest führt Ihre Funktion an jeder Schrittgrenze erneut von oben aus, sodass alles außerhalb eines Schritts jedes Mal wieder läuft. Und ein abgeschlossener Schritt wird memoisiert, sodass ein in Schritt eins gelöster Token in Schritt vier unverändert erneut eingespielt wird, lange nachdem er abgelaufen ist. Beide Regeln ergeben sich aus dem Ausführungsmodell und nicht aus dem Löser.

Was Sie brauchen

  • Eine Inngest-App mit einem serve-Endpunkt, normalerweise unter /api/inngest, und der Inngest Dev Server für lokale Läufe.
  • CapSkip läuft auf einer Windows-Maschine, und der Node-Client ist in der App installiert, die Ihre Funktionen bereitstellt.
  • Der sitekey und die Seiten-URL, die über die Event-Nutzdaten ankommen, statt fest in der Funktion zu stehen.
  • Server-Modus, wann immer die App irgendwo anders als auf der Maschine des Lösers bereitgestellt wird, und das gilt für die meisten Deployments. Es ist eine einzige Einstellung unter den Verbindungseinstellungen.
# npm install capskip
npm install inngest capskip

Schritt 1: Warum das Lösen innerhalb eines Schritts liegen muss

Inngest führt Ihre Funktion nicht einmal aus und arbeitet sie von oben nach unten ab. Es führt die Funktion aus, hält beim ersten Schritt an, zeichnet das Ergebnis auf und ruft die Funktion dann erneut von oben auf, mit dem angehängten Zustand der vorherigen Ausführung. Die eigene Dokumentation beschreibt den zweiten Durchlauf klar: Der Code des Schritts wird nicht ausgeführt, stattdessen fügt das SDK das Ergebnis in den Rückgabewert von step.run ein.

Das ist das ganze Modell, und es hat eine Konsequenz, die schwerer wiegt als alle anderen. Code, der außerhalb eines Schritts steht, wird nicht memoisiert und läuft daher bei jedem Aufruf. Eine Funktion mit vier Schritten ruft Ihren Handler viermal auf, sodass ein über den Schritten geschriebenes Lösen bei einem einzigen Funktionslauf viermal läuft.

// npm install capskip
// WRONG. This line runs once per step boundary, so a
// four-step function solves four CAPTCHAs for one run.
const result = await solver.recaptcha(sitekey, pageUrl);

await step.run("fetch-form", async () => { /* ... */ });
await step.run("submit", async () => { /* ... */ });

Inngest formuliert die Regel direkt: Jede nicht deterministische Logik, etwa Datenbankaufrufe oder API-Aufrufe, muss in einem step.run-Aufruf stehen. Ein Lösen ist ein API-Aufruf und gehört deshalb hinein. Bei einem abgerechneten Löser zeigt sich dieser Fehler als Rechnung. Bei einem lokalen Löser zeigt er sich als vierfache Arbeit und vier Token, von denen drei weggeworfen werden.

Schritt 2: Im selben Schritt lösen und absenden

Die zweite Regel ist weniger offensichtlich und schlägt erst später zu. Sobald ein Schritt abgeschlossen ist, wird sein Rückgabewert gespeichert und bei jedem weiteren Aufruf erneut eingespielt. Alle von step.run zurückgegebenen Daten werden als JSON serialisiert, und die Schritt-ID ist das, woran der Zustand memoisiert wird.

Ein Token, der aus einem Löse-Schritt zurückkommt, ist also ein gespeicherter String. Er kommt beim nächsten Aufruf und beim übernächsten identisch zurück, und bis dahin kann er Minuten alt sein. Ein reCAPTCHA-Token ist etwa zwei Minuten lang gültig. Alles, was zwischen dem Lösen und dem Absenden liegt, zehrt an diesem Fenster: ein Sleep, ein langsamer Fetch oder ein Schritt, der ein paar Mal mit Backoff wiederholt wurde. Jedes davon kann die Zeit ganz ablaufen lassen.

// WRONG. The token is memoized here and replayed later,
// by which time it has almost certainly expired.
const token = await step.run("solve", () =>
  solver.recaptcha(sitekey, pageUrl).then((r) => r.code)
);
await step.sleep("settle", "5m");
await step.run("submit", () => postForm(token));

Halten Sie beides zusammen. Ein Schritt löst und sendet ab und gibt nur das zurück, was der Rest der Funktion braucht, und das ist fast nie der Token selbst.

// RIGHT. The token is born and spent inside one step,
// so nothing expired is ever replayed.
const outcome = await step.run("solve-and-submit", async () => {
  const { code } = await solver.recaptcha(sitekey, pageUrl);
  const res = await postForm(pageUrl, code);
  return { status: res.status, id: res.id };
});

Dieses Ablauffenster ist dasselbe, das Leute erwischt, die Queue-Jobs verketten, und es lohnt sich, einmal nachzulesen: wie lange ein reCAPTCHA-Token gültig ist.

Schritt 3: Wiederholungen, und welche Fehler eine verdienen

Inngest wiederholt eine Funktion oder einen Schritt zusätzlich zum ersten Versuch viermal, und jeder step.run hat seinen eigenen unabhängigen Wiederholungszähler. Wiederholungen erfolgen mit exponentiellem Backoff und Jitter. Sie können die Option retries auf einen beliebigen Wert zwischen null und zwanzig setzen.

Für einen kombinierten Schritt aus Lösen und Absenden sind diese Standardwerte nahezu richtig, denn ein wiederholter Schritt führt seinen Code erneut aus und löst daher erneut. Es gibt keinen veralteten Token, der übernommen wird. Was sich zu justieren lohnt, ist die Frage, welche Fehler überhaupt eine Wiederholung 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 hinausVielleicht einmal. Selten vier Versuche wert
ApiExceptionDie API hat einen Fehlercode zurückgegebenKommt auf den Fehlercode an. Meistens nein
ValidationExceptionDie Parameter waren falsch und werden es wieder seinNein. Werfen Sie NonRetriableError
import { NonRetriableError } from "inngest";
import { ValidationException } from "capskip";

const outcome = await step.run("solve-and-submit", async () => {
  try {
    const { code } = await solver.recaptcha(sitekey, pageUrl);
    return await postForm(pageUrl, code);
  } catch (err) {
    // A bad sitekey will be bad on all five attempts.
    if (err instanceof ValidationException) {
      throw new NonRetriableError(err.message);
    }
    throw err;   // everything else gets the normal backoff
  }
});

NonRetriableError umgeht die verbleibenden Wiederholungen und lässt den Schritt fehlschlagen, aus dem er geworfen wurde, und genau das wollen Sie bei einer Anfrage, die fehlerhaft aufgebaut war und nicht bloß Pech hatte. Wenn der Löser Ihnen mitteilt, dass er ausgelastet und nicht defekt ist, können Sie mit RetryAfterError die Verzögerung selbst benennen, statt die Standardkurve zu nehmen.

Schritt 4: Die vollständige Funktion

Alles von oben, in einer Datei. Achten Sie darauf, wo der Client erzeugt wird: außerhalb des Handlers, damit er einmal pro Prozess entsteht und nicht einmal pro Aufruf, und er hält keinen Zustand pro Lauf.

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

export const inngest = new Inngest({ id: "signup-worker" });

// CAPSKIP_HOST is 127.0.0.1 locally and the solver machine
// once this app is deployed anywhere else.
const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: 8080,
});

export const submitSignup = inngest.createFunction(
  { id: "submit-signup", retries: 4 },
  { event: "signup/requested" },
  async ({ event, step }) => {
    const { sitekey, pageUrl, email } = event.data;

    // One step. The token never leaves it.
    const outcome = await step.run("solve-and-submit", async () => {
      const { code } = await solver.recaptcha(sitekey, pageUrl);
      return postSignup(pageUrl, email, code);
    });

    await step.run("record", () => saveResult(email, outcome));
    return outcome;
  }
);

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: Wo Ihr Code tatsächlich läuft und welchen Modus das erfordert

Inngest unterscheidet sich von den meisten gehosteten Automatisierungsplattformen auf eine Weise, die diesen Abschnitt bestimmt. Ihre Funktionen laufen nicht auf der Infrastruktur von Inngest. Inngest ruft Ihre Anwendung über HTTP an einem serve-Endpunkt auf, normalerweise unter /api/inngest, und Ihr Code wird in Ihrer eigenen App ausgeführt. Die Frage “kann das 127.0.0.1 erreichen” hat also nichts mit Inngest zu tun und alles damit, wo Sie bereitgestellt haben.

Wo die App bereitgestellt istWelcher Verbindungsmodus
Lokal, gegen den Dev Server, auf der CapSkip-MaschineLocal-Modus. 127.0.0.1 ist hier wirklich richtig
Auf Ihrem eigenen Server oder einer VM in Ihrem NetzwerkServer-Modus mit der LAN-Adresse des Solvers
Auf einem Serverless-Host wie Vercel oder LambdaServer-Modus mit einer statischen öffentlichen IP und einer Firewallregel
In einem Container neben der App, Löser anderswoServer-Modus. Loopback in einem Container ist der Container

Es gibt zwei Verbindungsmodi. Der Local-Modus bindet an 127.0.0.1 und antwortet nur diesem Gerät. Der Server-Modus bindet an Ihre Netzwerkadresse oder Ihre öffentliche IP, sodass ein anderer Rechner, ein Container-Host oder eine Serverless-Funktion dieselbe Windows-Maschine über die API erreichen kann. Beide finden Sie unter Verbindungseinstellungen, und 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.

Dass hier nicht pro Nutzung abgerechnet wird, macht es überhaupt vertretbar, eine Funktion den ganzen Tag feuern zu lassen.

Eines lässt sich leicht verwechseln: Auch Inngest Cloud muss Ihren serve-Endpunkt erreichen, und das ist ein eigenes Stück Netzwerk, getrennt vom Löser. Ein Deployment, das Inngest bereits aufrufen kann, ist nicht automatisch ein Deployment, das Ihr LAN erreichen kann.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Mehrere Lösungen für einen einzigen Funktionslauf protokolliertDas Lösen liegt außerhalb von step.run und wiederholt sich daher pro GrenzeVerschieben Sie es in einen Schritt
Das Absenden scheitert an einem Token, der sauber gelöst wurdeEin memoisierter Token wurde nach Ablauf erneut eingespieltLösen und senden Sie im selben Schritt ab
Ein Schritt wird viermal wiederholt und scheitert immer gleichEin Parameterfehler wird als vorübergehend behandeltWerfen Sie NonRetriableError bei ValidationException
NetworkException bei jedem Lauf nach dem DeploymentDie App ist von der Maschine des Lösers weggezogenServer-Modus, und setzen Sie CAPSKIP_HOST im Deployment
Ein Schrittergebnis hat zwischen zwei Deployments seine Form geändertDie Schrittausgabe wird als JSON serialisiert und über die ID zugeordnetBenennen Sie die Schritt-ID um, wenn sich ihr Rückgabewert ändert
CAPCHA_NOT_READY taucht bei einer selbst gebauten Polling-Schleife aufDas Ergebnis wurde gelesen, bevor es fertig warLassen Sie den Client pollen. Er drosselt von selbst
ERROR_WRONG_USER_KEY in einer ApiExceptionCAPSKIP_API_KEY ist in der Deployment-Umgebung nicht gesetztSetzen Sie ihn dort, wo die App läuft, und deployen Sie neu

Diese sechste Zeile begegnet den Leuten zuerst, wenn sie eine eigene Polling-Schleife schreiben, statt den Client zu verwenden. Die Schreibweise in dieser Antwort ist kein Tippfehler auf unserer Seite, denn die API gibt sie wirklich so zurück. Ausführlich erklärt wird sie im Leitfaden zu CAPCHA_NOT_READY.

FAQ

Kann ich den Token aus einem Schritt zurückgeben und später verwenden?

Können Sie, und es wird im Test funktionieren und in der Produktion scheitern. Der Wert wird gespeichert und bei jedem späteren Aufruf erneut eingespielt, sodass Sie in dem Moment, in dem irgendetwas Langsames zwischen den beiden Schritten liegt, einen Token absenden, der abgelaufen ist, während die Funktion gewartet hat. Halten Sie das Lösen und das, was den Token verbraucht, in einem Schritt, und geben Sie das Ergebnis zurück statt der Zugangsdaten.

Löst eine Wiederholung ein neues Captcha oder verwendet sie das alte erneut?

Ein neues. Ein fehlgeschlagener Schritt wird nicht memoisiert, sodass eine Wiederholung den Code darin erneut ausführt, und dazu gehört der Löse-Aufruf. Jeder step.run führt seinen eigenen unabhängigen Wiederholungszähler, sodass ein wackliger Schritt nicht das Budget der anderen aufbraucht. Genau deshalb kann der kombinierte Schritt gefahrlos wiederholt werden und ein aufgeteilter nicht.

Meine App läuft auf Vercel. Kann sie trotzdem einen Löser auf meinem Schreibtisch erreichen?

Ja, mit dem Server-Modus. Die Funktion läuft in der Sandbox von Vercel, sodass Loopback dort die Sandbox ist und nicht Ihre Maschine. Binden Sie CapSkip unter den Verbindungseinstellungen an Ihre öffentliche IP, setzen Sie eine Firewallregel davor, die nur das zulässt, was Sie erwarten, und setzen Sie CAPSKIP_HOST in der Projektumgebung. Eine statische öffentliche IP ist empfehlenswert, damit sich die Adresse nicht unbemerkt ändert.

Worin unterscheidet sich das davon, es in Temporal zu tun?

Beides ist durable execution, und beides landet auf unterschiedlichen Wegen bei derselben Regel. Temporal spielt einen Workflow aus seiner Ereignishistorie in einem Worker ab, den Sie selbst betreiben, also kommt die Regel aus dem Determinismus. Inngest ruft Ihren HTTP-Endpunkt erneut auf und fügt memoisierte Schrittergebnisse ein, also kommt die Regel aus dem Replay plus einem Token, der altert. Wie das bei Temporal aussieht, zeigt der Leitfaden zum Temporal-Workflow.

Die Kurzfassung

Setzen Sie das Lösen in step.run, niemals darüber, denn alles außerhalb eines Schritts läuft an jeder Schrittgrenze erneut. Setzen Sie das Lösen und das, was den Token verbraucht, in denselben Schritt, denn ein abgeschlossener Schritt wird memoisiert und erneut eingespielt, und ein Token lebt nur etwa zwei Minuten. Lassen Sie die Standardwerte für retries stehen, werfen Sie aber NonRetriableError bei Parameterfehlern. Setzen Sie CAPSKIP_HOST aus der Umgebung und betreiben Sie CapSkip im Server-Modus überall dort, wo die App nicht auf der Maschine des Lösers läuft.

Bevor Sie ein Parallelitätslimit für die Funktion festlegen, lohnt sich eine Überlegung: CapSkip ist ein Werkzeug zur Captcha-Umgehung und läuft auf Hardware, die Ihnen bereits gehört, sodass die einzige Obergrenze dafür, wie viele Läufe gleichzeitig lösen, diese Maschine ist und kein Guthaben.