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

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 Ausnahme | Was es bedeutet | Wiederholung sinnvoll? |
|---|---|---|
| NetworkException | CapSkip war nicht erreichbar oder startet gerade neu | Ja. Genau dafür sind Wiederholungen da |
| TimeoutException | Das Polling lief über die eigene Obergrenze des Clients hinaus | Vielleicht einmal. Selten vier Versuche wert |
| ApiException | Die API hat einen Fehlercode zurückgegeben | Kommt auf den Fehlercode an. Meistens nein |
| ValidationException | Die Parameter waren falsch und werden es wieder sein | Nein. 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 ist | Welcher Verbindungsmodus |
|---|---|
| Lokal, gegen den Dev Server, auf der CapSkip-Maschine | Local-Modus. 127.0.0.1 ist hier wirklich richtig |
| Auf Ihrem eigenen Server oder einer VM in Ihrem Netzwerk | Server-Modus mit der LAN-Adresse des Solvers |
| Auf einem Serverless-Host wie Vercel oder Lambda | Server-Modus mit einer statischen öffentlichen IP und einer Firewallregel |
| In einem Container neben der App, Löser anderswo | Server-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 sehen | Ursache | Beheben |
|---|---|---|
| Mehrere Lösungen für einen einzigen Funktionslauf protokolliert | Das Lösen liegt außerhalb von step.run und wiederholt sich daher pro Grenze | Verschieben Sie es in einen Schritt |
| Das Absenden scheitert an einem Token, der sauber gelöst wurde | Ein memoisierter Token wurde nach Ablauf erneut eingespielt | Lösen und senden Sie im selben Schritt ab |
| Ein Schritt wird viermal wiederholt und scheitert immer gleich | Ein Parameterfehler wird als vorübergehend behandelt | Werfen Sie NonRetriableError bei ValidationException |
| NetworkException bei jedem Lauf nach dem Deployment | Die App ist von der Maschine des Lösers weggezogen | Server-Modus, und setzen Sie CAPSKIP_HOST im Deployment |
| Ein Schrittergebnis hat zwischen zwei Deployments seine Form geändert | Die Schrittausgabe wird als JSON serialisiert und über die ID zugeordnet | Benennen Sie die Schritt-ID um, wenn sich ihr Rückgabewert ändert |
| CAPCHA_NOT_READY taucht bei einer selbst gebauten Polling-Schleife auf | Das Ergebnis wurde gelesen, bevor es fertig war | Lassen Sie den Client pollen. Er drosselt von selbst |
| ERROR_WRONG_USER_KEY in einer ApiException | CAPSKIP_API_KEY ist in der Deployment-Umgebung nicht gesetzt | Setzen 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.
- Die rohen Endpunkte hinter dem Client sind dokumentiert in der CapSkip-API-Dokumentation.
- Die Checkbox-Challenge selbst erklärt Ihnen die reCAPTCHA-v2-Solver-Seite.
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.
