So lösen Sie Captchas in einem Laravel-Queue-Job (PHP SDK)

Um ein Captcha in einem Laravel-Queue-Job zu lösen, rufen Sie den PHP-Client von CapSkip in der handle-Methode des Jobs auf und senden den Token aus demselben Job ab. Der Aufruf ist kurz. Sorgfalt verlangen drei Zahlen, die Laravel für Jobs mitbringt, die in Sekunden fertig sind: Der Worker gibt jedem Job 60 Sekunden, die Queue übergibt einen Job nach 90 Sekunden an einen anderen Worker, und jeder Job bekommt einen einzigen Versuch. Ein reCAPTCHA-Lösevorgang kann bis zu 300 Sekunden pollen. Mit den Standardwerten wird ein Laravel-Queue-Captcha-Job also entweder mittendrin beendet oder als fehlgeschlagen markiert, während er noch löst, und sobald Sie Retries hinzufügen, kann er zweimal laufen. Dieser Leitfaden behandelt den Job, die Timeouts, eine Retry-Regel und das Einzige, was sich ändert, wenn der Worker unter Windows läuft.
Was Sie brauchen
- Laravel 11 oder neuer. Der Job verwendet den einzelnen Trait Queueable, den Version 11 eingeführt hat; alles andere hier funktioniert auch unter 10. Laravel 13 kann die Job-Einstellungen zusätzlich als Attribute wie #[Tries(3)] ausdrücken, und die einfachen Eigenschaften, die unten verwendet werden, funktionieren dort weiterhin.
- Das PHP-Paket von CapSkip, das PHP 8.0 oder neuer mit den Erweiterungen curl und json braucht. Prüfen Sie, ob curl in der php.ini aktiviert ist, die Ihr Worker tatsächlich lädt, denn unter Windows ist die Zeile manchmal noch auskommentiert.
- Eine Queue-Verbindung. Neue Laravel-Apps verwenden den Treiber database, und die Beispiele unten tun das auch.
- CapSkip auf einem Windows-Rechner. Im Local-Modus antwortet es auf 127.0.0.1 nur für diesen Rechner, was zu einem Worker auf demselben PC passt. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, sodass eine Laravel-App auf einem Linux-Server, auf Forge oder auf jedem anderen Host es über die API erreichen kann. Eingestellt werden beide Modi unter Verbindungseinstellungen.
# Run in the Laravel project root composer require capskip/capskip
Schritt 1: Den Client im Container registrieren
Halten Sie die Adresse des Solvers in der Konfiguration statt im Job, damit derselbe Code gegen einen lokalen Solver und gegen einen Solver auf einem Server läuft. Fügen Sie einen Block zu config/services.php und ein Singleton-Binding zu Ihrem Service Provider hinzu.
// config/services.php
'capskip' => [
'key' => env('CAPSKIP_API_KEY', 'capskip'),
'host' => env('CAPSKIP_HOST', '127.0.0.1'),
'port' => (int) env('CAPSKIP_PORT', 8080),
],
// app/Providers/AppServiceProvider.php, inside register()
$this->app->singleton(\CapSkip\CapSkip::class, fn () => new \CapSkip\CapSkip([
'apiKey' => config('services.capskip.key'),
'host' => config('services.capskip.host'),
'port' => config('services.capskip.port'),
]));Der Client liest Umgebungsvariablen nie von selbst, deshalb übergibt das Binding jeden Wert einzeln. Lesen Sie sie überall sonst in der App über config() statt über env(): Sobald Sie config:cache ausführen, gibt env() außerhalb der Konfigurationsdateien null zurück.
Schritt 2: Lösen und Absenden in einem Job
Geben Sie den Client als Type-Hint in handle() an, und Laravel injiziert das Singleton. Der Job liest den sitekey von der Seite, löst und sendet das Formular ab.
<?php
namespace App\Jobs;
use CapSkip\CapSkip;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;
class SubmitSignup implements ShouldQueue
{
use Queueable;
public function __construct(public string $pageUrl, public array $fields) {}
public function handle(CapSkip $solver): void
{
$html = Http::timeout(30)->get($this->pageUrl)->throw()->body();
preg_match('/data-sitekey="([^"]+)"/', $html, $m);
// Solve and submit in one job: the token expires in two minutes.
$token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
Http::asForm()->timeout(30)
->post($this->pageUrl, $this->fields + ['g-recaptcha-response' => $token])
->throw();
}
}Dispatchen Sie ihn mit der Seite und den übrigen Feldern des Formulars auf seine eigene Verbindung und Queue, die der nächste Schritt definiert:
SubmitSignup::dispatch('https://example.com/signup', ['email' => 'YOUR_EMAIL'])
->onConnection('captcha')
->onQueue('captcha');Halten Sie Lösen und Absenden zusammen. Ein reCAPTCHA-Token ist etwa zwei Minuten gültig, und ein Token, der an einen zweiten, verketteten Job übergeben wird, kann diese Zeit mit Warten hinter anderer Arbeit verbringen. Der Leitfaden zur Gültigkeitsdauer von reCAPTCHA-Tokens behandelt diese Frist im Detail. Der PHP-Client ist synchron, der Worker-Prozess ist also während des gesamten Lösevorgangs belegt. Um in Laravel Lösevorgänge parallel auszuführen, brauchen Sie mehr Worker-Prozesse, keinen anderen Client.
Schritt 3: Die Timeouts aufeinander abstimmen
Bei einem Laravel-Queue-Captcha-Job laufen vier Zeitlimits. Von innen nach außen:
- Die eigenen Einstellungen von CapSkip. Eine reCAPTCHA-Aufgabe kann bis zu 250 Sekunden auf einen freien Thread warten (Wait Timeout) und bekommt 250 Sekunden zum Lösen (Row Timeout). Läuft eines davon ab, bricht CapSkip die Aufgabe ab, und der Client löst eine ApiException aus, sofern nicht das unten beschriebene eigene Limit des Clients schon vorher gegriffen hat.
- Das Polling des Clients. recaptcha() pollt höchstens so lange wie recaptchaTimeout, standardmäßig 300 Sekunden, und löst dann eine TimeoutException aus. Da die beiden HTTP-Aufrufe drumherum oben auf je 30 Sekunden begrenzt sind, ist das gesamte handle() innerhalb von etwa sechs Minuten fertig, wenn CapSkip normal antwortet. Die Frist von 300 Sekunden wird allerdings nur zwischen den Polls geprüft, und jede Anfrage, die der Client sendet, kann bis zu 120 Sekunden hängen. Ein CapSkip, das nicht mehr antwortet, kann einen Job also darüber hinaus verlängern.
- Das Job-Timeout. Die Option timeout des Workers steht standardmäßig auf 60 Sekunden, und bei einem Job, der länger läuft, wird der Worker-Prozess hart beendet. Setzen Sie am Job ein $timeout, das länger ist als die sechs Minuten oben.
- retry_after. Jede Queue-Verbindung gibt einen reservierten Job an die Queue zurück, sobald er so lange läuft, standardmäßig 90 Sekunden. Hat der Job noch Versuche übrig, startet ein langsamer Lösevorgang, der auf einem Worker noch läuft, auf einem anderen erneut, was einen zweiten Lösevorgang und ein zweites Absenden des Formulars bedeutet. Hat er keine mehr, markiert ihn der zweite Worker sofort als fehlgeschlagen, während der erste das Formular womöglich trotzdem noch absendet.
In der Laravel-Dokumentation heißt es, dass das Timeout immer mindestens einige Sekunden kürzer als retry_after sein sollte. Die Reihenfolge lautet also: zuerst der Client, dann das Job-Timeout, zuletzt retry_after. Geben Sie Captcha-Jobs eine eigene Verbindung, damit das lange retry_after nicht die Wiederaufnahme aller anderen Jobs in der App verzögert:
// config/queue.php, under 'connections'
'captcha' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => 'captcha',
// Longer than the job's timeout, so no second worker takes it.
'retry_after' => 420,
'after_commit' => false,
],
// On the job class
public $timeout = 390;Starten Sie den Worker für diese Verbindung und Queue:
php artisan queue:work captcha --queue=captcha
Das $timeout des Jobs hat Vorrang vor der Option timeout des Workers, der Befehl braucht also keine.
Schritt 4: Nur wiederholen, was ein Retry beheben kann
Ein Job bekommt einen einzigen Versuch, sofern Sie nichts anderes angeben, daher lässt ihn jede Exception endgültig fehlschlagen. Manche Captcha-Fehler sind einen weiteren Versuch wert: ein Ergebnis ERROR_CAPTCHA_UNSOLVABLE, eine TimeoutException oder eine NetworkException, weil CapSkip gerade neu startete. Andere gelingen bei einer Wiederholung nie: ein falscher API-Schlüssel, ein fehlerhafter sitekey (ERROR_GOOGLEKEY) oder ein Parameter, den der Client ablehnt. Ein sitekey, den Google ablehnt, kommt als ERROR_CAPTCHA_UNSOLVABLE zurück und verbraucht daher die Retries wie jedes andere unlösbare Ergebnis. Erlauben Sie drei Versuche, verteilen Sie sie zeitlich, und lassen Sie die hoffnungslosen Fälle sofort fehlschlagen.
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\ValidationException;
// On the job class
public $tries = 3;
public $backoff = [30, 120];
// Inside handle(), around the solve
try {
$token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
} catch (ValidationException $e) {
$this->fail($e); // a bad parameter will not fix itself
return;
} catch (ApiException $e) {
if (! str_contains($e->getMessage(), 'UNSOLVABLE')) {
$this->fail($e); // a wrong API key and the like
return;
}
throw $e; // retried after the backoff
}$this->fail() marks the job failed without spending the remaining attempts, and anything rethrown is retried after 30 seconds, then 120. A job killed by its timeout also uses up an attempt, but it is picked up again only when retry_after expires, not after the backoff. A 4xx from the form also throws, and the retry solves and posts again. If the site answers 4xx for bad input, catch Illuminate\Http\Client\RequestException around the post and call $this->fail($e) when $e->response->clientError() is true.
Den Worker unter Windows betreiben
Laravel auf demselben Windows-PC wie CapSkip zu betreiben, ist das einfachste Setup, und es hat eine Falle. Laravel setzt Job-Timeouts mit der Erweiterung pcntl durch, und pcntl gibt es unter Windows nicht. Dort gibt dieser Befehl bool(false) aus:
php -r "var_dump(extension_loaded('pcntl'));"Ohne pcntl werden das $timeout des Jobs und die Option timeout des Workers stillschweigend ignoriert, und ein Job läuft, bis er zurückkehrt, egal wie lange das dauert. retry_after gilt weiterhin, denn es wird geprüft, wenn der nächste Worker einen Job abholt, und nicht per Signal durchgesetzt. Unter Windows hält in Laravel also nichts einen langsamen Job an. Was einen Lösevorgang beendet, sind Wait Timeout und Row Timeout von CapSkip und das Polling-Limit des Clients von 300 Sekunden, lassen Sie diese also bestehen. Halten Sie retry_after über der längsten Zeit, die ein Job brauchen kann: 420 reicht für ein CapSkip, das normal antwortet, und 660 auch für eine Verbindung, die hängen bleibt, zum Beispiel im Server-Modus über das Internet.
Zwei weitere Dinge sind anders. Laravel Horizon braucht die Erweiterungen pcntl und posix und lässt sich daher unter Windows nicht installieren; verwenden Sie dort einfaches queue:work, oder betreiben Sie Horizon auf einem Linux-Host, der CapSkip im Server-Modus aufruft. Und Supervisor gibt es dort nicht, betreiben Sie jeden Worker also unter einer geplanten Aufgabe oder einem Service-Wrapper, der ihn neu startet. Unter Linux muss stopwaitsecs von Supervisor länger sein als Ihr längster Job, sonst beendet ein Deployment einen Lösevorgang mittendrin. In beiden Fällen führen Sie nach einem Deployment oder einer Änderung an .env php artisan config:cache aus, falls Sie die Konfiguration cachen, und danach php artisan queue:restart, denn ein Worker behält die Konfiguration, mit der er gestartet ist.
Jeder Worker-Prozess löst ein Captcha nach dem anderen, und CapSkip führt standardmäßig bis zu 10 reCAPTCHA-Lösevorgänge gleichzeitig aus (Max. Threads in seinen reCAPTCHA-Einstellungen). Mehr als etwa zehn Worker auf der Queue captcha lassen Jobs nur in CapSkip warten, wo die Zeit auf das Wait Timeout angerechnet wird.
Vollständiges lauffähiges Beispiel
<?php
namespace App\Jobs;
use CapSkip\CapSkip;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\ValidationException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Throwable;
class SubmitSignup implements ShouldQueue
{
use Queueable;
public $tries = 3;
public $backoff = [30, 120];
// GET 30 s + solve up to 300 s + POST 30 s, plus a margin.
public $timeout = 390;
public function __construct(public string $pageUrl, public array $fields) {}
public function handle(CapSkip $solver): void
{
$html = Http::timeout(30)->get($this->pageUrl)->throw()->body();
if (! preg_match('/data-sitekey="([^"]+)"/', $html, $m)) {
$this->fail(new RuntimeException("No sitekey on {$this->pageUrl}"));
return;
}
try {
$token = $solver->recaptcha($m[1], $this->pageUrl)['code'];
} catch (ValidationException $e) {
$this->fail($e);
return;
} catch (ApiException $e) {
if (! str_contains($e->getMessage(), 'UNSOLVABLE')) {
$this->fail($e);
return;
}
throw $e;
}
// Submit in the same job, while the token is still valid.
Http::asForm()->timeout(30)
->post($this->pageUrl, $this->fields + ['g-recaptcha-response' => $token])
->throw();
}
public function failed(?Throwable $exception): void
{
logger()->warning('Signup gave up', [
'url' => $this->pageUrl,
'error' => $exception?->getMessage(),
]);
}
}Kombinieren Sie ihn mit der Verbindung captcha aus Schritt 3 und dem Binding aus Schritt 1, dispatchen Sie ihn wie in Schritt 2 gezeigt, und starten Sie einen Worker mit dem Befehl aus Schritt 3. Das sitekey-Muster erwartet das Attribut data-sitekey in doppelten Anführungszeichen, so wie das eigene Snippet von reCAPTCHA es schreibt. Für ein Invisible- oder Enterprise-Widget übergeben Sie die passende Option an recaptcha(). Wie das geht, zeigt die reCAPTCHA-v2-Solver-Seite; den rohen Endpunkt hinter dem Aufruf finden Sie in der API-Referenz.
Häufige Fehler und was sie bedeuten
| Was Sie sehen | Ursache | Beheben |
|---|---|---|
| "has timed out" nach 60 Sekunden, und der Job wird als fehlgeschlagen markiert | Das Standard-Timeout des Workers beträgt 60 Sekunden, und der Job hat einen einzigen Versuch | Setzen Sie $timeout am Job, und $tries, wenn Sie Retries wollen |
| Das Formular wird zweimal abgesendet, oder für einen Job laufen zwei Lösevorgänge | Der Job lief länger als retry_after, standardmäßig 90 Sekunden, und ein zweiter Worker hat ihn übernommen | Legen Sie Captcha-Jobs auf eine Verbindung, deren retry_after länger ist als $timeout |
| Unter Windows läuft ein Job weit über sein $timeout hinaus | Job-Timeouts brauchen pcntl, und das hat Windows nicht | Verlassen Sie sich auf die eigenen Timeouts von CapSkip und das Limit des Clients von 300 Sekunden, und halten Sie retry_after über der Dauer des gesamten Jobs |
| "has been attempted too many times" | Der Job wurde erneut aufgenommen, nachdem sein Worker gestorben war oder er retry_after überschritten hatte, und zwar öfter, als $tries erlaubt | Suchen Sie nach Workern, die ein Deployment mitten im Job beendet hat, und nach Jobs, die retry_after überdauern |
| ApiException mit ERROR_CAPTCHA_UNSOLVABLE | CapSkip hat die Aufgabe abgebrochen, zum Beispiel weil sie über das Row Timeout hinaus lief | Werfen Sie sie erneut, damit Laravel nach dem Backoff wiederholt |
| ApiException mit ERROR_KEY_DOES_NOT_EXIST | Die Validierung des API-Schlüssels ist in CapSkip aktiv, und CAPSKIP_API_KEY passt zu keinem Schlüssel dort | Korrigieren Sie den Schlüssel und starten Sie die Worker neu; lassen Sie den Job fehlschlagen, statt ihn zu wiederholen |
| NetworkException beim ersten Lösevorgang nach einer Änderung an .env | Der Worker läuft noch mit dem alten Host, oder CapSkip läuft nicht | Führen Sie php artisan config:cache aus, falls Sie die Konfiguration cachen, dann php artisan queue:restart, und prüfen Sie, ob Local-Modus oder Server-Modus eingestellt ist |
| Call to undefined function curl_init() | Die Erweiterung curl ist in der php.ini, die der Worker lädt, deaktiviert | Aktivieren Sie extension=curl in dieser php.ini |
| Die Seitenanfrage bricht mit einem Timeout ab, während der Job "läuft" | Der Job wurde ohne die Verbindung captcha dispatcht, und QUEUE_CONNECTION ist sync, also lief er innerhalb der Webanfrage | Dispatchen Sie ihn wie in Schritt 2 auf die Verbindung captcha, und lassen Sie einen Worker darauf laufen |
| Die Website lehnt den Token ab | Er wurde nach seinem Ablauf abgesendet oder an die falsche URL | Senden Sie direkt nach dem Lösen aus demselben Job ab, an die Action-URL des Formulars |
FAQ
Warum das Captcha nicht im Controller lösen?
Weil ein Lösevorgang Dutzende Sekunden braucht und mehrere Minuten dauern kann. Das ist länger, als ein Besucher wartet, und länger als die max_execution_time von 30 Sekunden, die die meisten PHP-Installationen einer Webanfrage geben. Wenn Sie den Job in die Queue stellen, kann der Controller die Antwort sofort zurückgeben, während der langsame Teil in einem Worker-Prozess läuft, der keine solche Grenze hat.
Läuft ein Laravel-Queue-Captcha-Job unter Horizon?
Ja, auf einem Host mit pcntl, also unter Linux oder macOS. Horizon setzt das Timeout pro Supervisor in config/horizon.php. Geben Sie dem Supervisor, der die Queue captcha abarbeitet, also ein Timeout knapp über den 390 Sekunden des Jobs, etwa 400, denn mit der Balancing-Strategie auto beendet Horizon beim Herunterskalieren Worker, die länger laufen als sein eigenes Timeout. Halten Sie retry_after der Redis-Verbindung darüber (420 funktioniert) und richten Sie CAPSKIP_HOST auf den Windows-Rechner, auf dem CapSkip im Server-Modus läuft.
Kann eine Laravel-App auf einem Linux-Server CapSkip auf meinem Windows-PC nutzen?
Ja. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, damit es auf einer Netzwerkadresse lauscht, setzen Sie CAPSKIP_HOST in der .env der App und starten Sie die Worker neu. Verwenden Sie eine statische öffentliche IP und eine Firewall-Regel, die nur Ihren Server zulässt, wenn der Weg über das Internet führt, und aktivieren Sie die Validierung des API-Schlüssels. Der Solver bleibt auf Ihrem eigenen Windows-Rechner, an der Art, wie Lösungen gezählt werden, ändert sich also nichts.
Wie unterscheidet sich das von Hangfire oder Celery?
Die Grundform ist überall gleich: Lösen und Absenden in einem Job, nur die Fehler wiederholen, die ein Retry beheben kann, und die Zahl der Worker an die Thread-Anzahl von CapSkip anpassen. Unterschiedlich ist, welcher Standardwert Ihnen in die Quere kommt. Laravel gibt einen einzigen Versuch und übergibt einen langsamen Job an einen zweiten Worker, der ihn als fehlgeschlagen markiert oder, sobald Sie Retries erlauben, erneut ausführt; Hangfire wiederholt zehnmal über Stunden hinweg. Die .NET-Variante finden Sie im Leitfaden zu Captcha-Jobs mit Hangfire.
Die Kurzfassung
Binden Sie für einen Laravel-Queue-Captcha-Job den CapSkip-Client im Container, und erledigen Sie Lösen und Absenden im selben Job. Geben Sie Captcha-Jobs eine eigene Verbindung mit einem retry_after von 420 Sekunden, setzen Sie $timeout auf 390 und $tries auf 3 mit einem Backoff, und lassen Sie den Job bei Fehlern, die ein Retry nicht beheben kann, fehlschlagen. Denken Sie unter Windows daran, dass ohne pcntl kein Timeout durchgesetzt wird, und starten Sie die Worker nach jedem Deployment neu.
- Alle Captcha-Typen, die das PHP-Paket verarbeitet, mit einfachen PHP-Beispielen: die PHP-Captcha-Solver-Seite.
- Derselbe reCAPTCHA-Aufruf außerhalb von Laravel: reCAPTCHA v2 in PHP lösen.
Bei Retries zahlen sich diese Einstellungen aus. Ein Job, der ein unlösbares Captcha zweimal wiederholt, ist nichts Besonderes, wenn der Captcha-Löser auf Ihrem eigenen Rechner läuft, denn jeder zusätzliche Versuch kostet dort Worker- und Thread-Zeit statt einer weiteren nach Verbrauch abgerechneten Lösung.
