Captchas in einer Azure Function lösen (C# Isolated)

azure functions captcha - How to Solve CAPTCHAs in an Azure Function (C# Isolated)

Eine Captcha-Lösung in Azure Functions bricht aus einem Grund ab, den Ihr Code Ihnen nie zeigt. Eine HTTP-getriggerte Funktion hat 230 Sekunden Zeit, um auf eine Anfrage zu antworten, egal welches Timeout Sie konfigurieren, denn diese Grenze kommt vom Load Balancer vor der Plattform. Das reCAPTCHA-Polling-Timeout im CapSkip-Client steht standardmäßig auf 300 Sekunden. Eine langsame Lösung wird also von Azure abgeschnitten und nicht von Ihrer Funktion, und im Log steht eine Anfrage, die einfach endet. Die Abhilfe besteht darin, gar nicht mehr auf der HTTP-Anfrage zu lösen. Das Zweite, was stimmen muss, ist Loopback, denn eine Function App läuft auf den Maschinen von Azure und nicht auf Ihren.

Was Sie brauchen

  • Eine .NET-Function-App mit Isolated Worker. Die Unterstützung für das In-Process-Modell endet am 10. November 2026, der Isolated Worker ist also das Modell, auf dem Sie aufbauen sollten.
  • CapSkip läuft auf einer Windows-Maschine, im Server-Modus, unter einer Adresse, die die Function App erreichen kann.
  • Ein Speicherkonto, denn das Muster unten verlagert das Lösen auf eine Queue-getriggerte Funktion.
  • Der sitekey und die Seiten-URL kommen über die Queue-Nachricht herein statt fest im Code zu stehen, damit eine Funktion jedes Formular bedient.
# dotnet add package CapSkip
dotnet add package CapSkip
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues

Schritt 1: Loopback in einer Function App ist die Function App

Das klären Sie vor allem anderen, denn davon hängt ab, ob der Rest funktioniert. Ihre Funktion läuft auf einer Instanz, die Azure bereitstellt, 127.0.0.1 darin ist also diese Instanz. Dort lauscht nichts auf Port 8080, und der Fehler kommt als NetworkException bei der ersten Lösung nach dem Deploy, während derselbe Code unter dem lokalen Tooling einwandfrei lief.

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 die Funktion betreibenWelcher Verbindungsmodus
Lokales Tooling, auf der CapSkip-MaschineLocal-Modus. 127.0.0.1 ist hier wirklich richtig
Deployed, mit dem Löser in einem Netzwerk, das Azure routen kannServer-Modus mit dieser privaten Adresse
Deployed, mit dem Löser über das Internet erreichbarServer-Modus mit einer statischen öffentlichen IP und einer Firewallregel

Wenn der Löser in einem Netzwerk sitzt, zu dem Azure routen kann, lohnt es sich, zwei Azure-Features zu kennen. Die Integration in ein virtuelles Netzwerk für ausgehenden Datenverkehr gibt es in den Plänen Flex Consumption, Premium und Dedicated, und im alten Consumption-Plan gibt es sie überhaupt nicht. Hybrid Connections, die dafür gebaut sind, einen Dienst zu erreichen, der im eigenen Netzwerk bleibt, gibt es in den Plänen Premium und Dedicated für Apps, die unter Windows laufen. In beiden Fällen bleibt der Löser auf Ihrer Hardware; nur der Weg ändert sich.

Legen Sie die Adresse in eine Anwendungseinstellung statt in den Quellcode, denn das lokale Tooling und die deployte App brauchen unterschiedliche Werte. Der Client liest von sich aus keinerlei Umgebungsvariablen, lesen Sie CAPSKIP_HOST also in Ihrem Code aus und übergeben Sie den Wert, wie es die Funktion unten tut.

// dotnet add package CapSkip
using CapSkip;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

// One client for the app. The host is an application
// setting, so local and deployed can differ.
builder.Services.AddSingleton(new CapSkipClient(
    host: Environment.GetEnvironmentVariable("CAPSKIP_HOST") ?? "127.0.0.1",
    port: 8080));

builder.Build().Run();

Schritt 2: Die 230-Sekunden-Wand, und warum sie nicht Ihr Timeout ist

Das ist der Teil, der einen Nachmittag verschlingt, denn jede Zahl, die Sie im Portal sehen, ist größer als die, die die Anfrage tatsächlich abbricht.

Microsoft dokumentiert es unmissverständlich: Unabhängig von der Timeout-Einstellung der Function App sind 230 Sekunden die maximale Zeit, die eine HTTP-getriggerte Funktion für die Antwort auf eine Anfrage brauchen darf, und diese Grenze existiert wegen des Standard-Idle-Timeouts im Azure Load Balancer. Sie können sie weder über host.json noch über eine Anwendungseinstellung noch durch einen Planwechsel anheben.

Stellen Sie nun die Zahlen des Clients daneben. Das Polling-Timeout für reCAPTCHA, Turnstile und GeeTest steht standardmäßig auf 300 Sekunden, das Timeout für Bilder auf 120 Sekunden. Eine Bildlösung passt also bequem innerhalb der Wand, und eine reCAPTCHA-Lösung darf 70 Sekunden darüber hinaus laufen. Die meisten Lösungen sind lange vor beiden Zahlen fertig, und genau deshalb geht so etwas in Produktion und scheitert dann am langsamen Ausläufer.

LimitWertLässt es sich ändern?
HTTP-Antwort, jeder Plan230 SekundenNein
reCAPTCHA-Polling-Timeout des Clients300 SekundenJa, im Konstruktor
Bild-Polling-Timeout des Clients120 SekundenJa, im Konstruktor

Das reCAPTCHA-Polling-Timeout unter 230 Sekunden zu senken lohnt sich trotzdem, denn ein Client, der zuerst aufgibt, erzeugt eine CapSkip.TimeoutException, die Sie loggen können, statt einer Anfrage, die verschwindet. Die eigentliche Abhilfe ist das aber nicht. Die eigentliche Abhilfe steht in der Azure-Dokumentation: Verwenden Sie das asynchrone Muster von Durable Functions, oder verschieben Sie die eigentliche Arbeit und antworten Sie sofort. In der Praxis heißt das: Der HTTP-Trigger nimmt den Auftrag an, schreibt eine Nachricht und kehrt sofort zurück.

[Function(nameof(EnqueueSolve))]
[QueueOutput("captcha-jobs")]
public SolveRequest EnqueueSolve(
    [HttpTrigger(AuthorizationLevel.Function, "post")] SolveRequest req)
{
    // Returns in milliseconds. The solve happens on the
    // queue-triggered function, off the HTTP request.
    return req;
}

Schritt 3: Das Timeout der Function App ist eine eigene Grenze

Sobald das Lösen von der HTTP-Anfrage weg ist, zählt das Timeout in host.json, und das hängt vom Plan ab. Die Standardwerte sind überall großzügig, außer im alten Consumption-Plan, und das ist der eine Plan, in dem einer langsamen reCAPTCHA-Lösung wirklich der Platz ausgehen kann.

Hosting-PlanStandard-TimeoutMaximales Timeout
Flex-Consumption-Plan30 MinutenKein erzwungenes Maximum
Premium-Plan30 MinutenKein erzwungenes Maximum
Dedicated-Plan30 MinutenKein erzwungenes Maximum, mit Always On
Consumption-Plan, veraltet5 Minuten10 Minuten

Fünf Minuten sind genau 300 Sekunden, der alte Plan deckt ein reCAPTCHA-Timeout im Standard also nicht ab: Die Funktion stirbt genau in dem Moment, in dem der Client aufgegeben hätte. Erhöhen Sie den Wert, wenn Sie noch auf diesem Plan sind, und halten Sie das eigene Timeout des Clients unter dem, was Sie einstellen.

{
  "version": "2.0",
  "functionTimeout": "00:10:00"
}

Schritt 4: Wie oft eine Queue-Nachricht erneut gelöst wird

Das Lösen auf eine Queue zu verlagern verschafft Ihnen Luft, und es bringt ein eigenes Wiederholungsverhalten mit, das echte Zeit kostet, wenn Sie es so lassen.

Wenn eine Queue-getriggerte Funktion fehlschlägt, führt Azure Functions die Funktion für diese Nachricht bis zu fünfmal aus, den ersten Versuch eingerechnet. Schlagen alle fünf fehl, schreibt die Runtime die Nachricht in eine Queue, die nach dem Original benannt ist und das Suffix poison trägt. Das sind fünf Lösungen für ein Captcha, wenn der Fehler etwas ist, das nie gelingen wird, etwa ein sitekey, der nicht zur Seite gehört.

Es wird enger, als es aussieht. Das visibilityTimeout in host.json steht standardmäßig auf null, eine fehlgeschlagene Nachricht taucht also sofort wieder auf, und diese fünf Versuche können in Sekunden hintereinander ablaufen. Setzen Sie es auf einen Wert, der einem vorübergehenden Problem Zeit gibt, sich zu klären, und lesen Sie den Dequeue-Count in der Funktion aus, damit eine Nachricht in ihrem letzten Versuch anders behandelt werden kann.

Die andere Hälfte ist die Nebenläufigkeit. Standardmäßig holt sich der Trigger einen Batch von 16 Nachrichten und dann weitere 16, sobald die Zahl der noch laufenden auf 8 fällt. Diese 8 laufen noch, während der neue Batch startet, eine einzelne Instanz kann also 24 Lösungen gleichzeitig für eine Funktion laufen haben. Skaliert die App heraus, multipliziert sich diese Zahl mit der Anzahl der Instanzen. Bei einem Dienst mit Abrechnung pro Lösung würden Sie das deckeln, um ein Guthaben zu schützen. Hier ist es eine Kapazitätsfrage zu einer Windows-Maschine, aber 24 gleichzeitige Lösungen pro Instanz sind trotzdem eine Entscheidung, die Sie bewusst treffen sollten, statt sie zu erben.

Das Beispiel unten bleibt bewusst unter beiden Standardwerten: drei Versuche statt fünf und ein Batch von acht statt sechzehn.

{
  "version": "2.0",
  "extensions": {
    "queues": {
      "batchSize": 8,
      "newBatchThreshold": 4,
      "visibilityTimeout": "00:00:30",
      "maxDequeueCount": 3
    }
  }
}

Vollständiges lauffähiges Beispiel

Die Queue-getriggerte Hälfte, mit dem Lösen und dem Absenden im selben Aufruf.

// dotnet add package CapSkip
using CapSkip;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class SolveCaptcha(CapSkipClient solver, ILogger<SolveCaptcha> log)
{
    [Function(nameof(SolveCaptcha))]
    public async Task Run([QueueTrigger("captcha-jobs")] SolveRequest job)
    {
        try
        {
            // Solve and submit together. The token is short lived.
            var result = await solver.RecaptchaAsync(job.Sitekey, job.PageUrl);
            await SubmitFormAsync(job.PageUrl, result.Code);
        }
        catch (CapSkip.ValidationException ex)
        {
            // A bad sitekey fails identically on all five tries.
            log.LogError("Not retryable: {Message}", ex.Message);
        }
    }
}

Dieser Aufruf ist reCAPTCHA v2. Die anderen Typen haben dieselbe Form: Übergeben Sie ein Options-Dictionary mit invisible oder enterprise auf 1, oder version auf v3 mit einer action, oder rufen Sie stattdessen TurnstileAsync oder GeetestAsync auf. Die vollständige Übersicht finden Sie auf der C#-Captcha-Solver-Seite.

Turnstile auf einer Challenge-Seite ist die eine Ausnahme, die Sie kennen sollten, denn dafür braucht es zwei zusätzliche Werte aus der Seite, dazu den User Agent, den der Löser verwendet hat. Dafür gibt es einen eigenen Leitfaden.

Dass der Parameterfehler geschluckt und nicht erneut geworfen wird, ist Absicht. Eine geworfene Exception startet den Zyklus aus fünf Versuchen, und gegen einen sitekey, der nicht zur Seite passt, kann ein Retry nichts ausrichten.

Häufige Fehler und was sie bedeuten

Was Sie sehenUrsacheBeheben
Die HTTP-Anfrage endet nach etwa vier Minuten ohne FehlerDie 230-Sekunden-Grenze des Load Balancers, nicht Ihr TimeoutSofort antworten und in einem Queue-Trigger lösen
Läuft mit dem lokalen Tooling, NetworkException nach dem DeployLoopback in einer Function App ist die Azure-InstanzServer-Modus, und setzen Sie CAPSKIP_HOST in den Anwendungseinstellungen
Fünf identische Fehlschläge, dann eine Nachricht in der Poison-QueueEin nicht wiederholbarer Fehler wurde aus der Funktion geworfenFangen Sie CapSkip.ValidationException ab und loggen Sie sie stattdessen
Fünf Versuche in unter einer Minute verbranntDas visibilityTimeout der Queue steht standardmäßig auf nullSetzen Sie ein visibilityTimeout, damit die Retries auseinanderliegen
Der Build scheitert an einer mehrdeutigen TimeoutExceptionCapSkip und System definieren beide diesen KurznamenQualifizieren Sie ihn, oder fangen Sie den Basistyp CapSkipError ab
ERROR_WRONG_USER_KEY in einer ApiExceptionCAPSKIP_API_KEY ist in der deployten App nicht gesetztFügen Sie ihn den Anwendungseinstellungen hinzu und starten Sie die App 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 eine Function App in Azure einen Löser in meinem eigenen Netzwerk erreichen?

Ja. Stellen Sie CapSkip unter Verbindungseinstellungen auf den Server-Modus um, damit es auf einer Netzwerkadresse statt auf Loopback lauscht, und richten Sie CAPSKIP_HOST in den Anwendungseinstellungen darauf aus. Für einen privaten Weg gibt es die Integration in ein virtuelles Netzwerk in den Plänen Flex Consumption, Premium und Dedicated und Hybrid Connections in Premium und Dedicated für Apps, die unter Windows laufen. Gehen Sie stattdessen über das Internet, verwenden Sie eine statische öffentliche IP mit einer Firewall-Regel, die nur die erwarteten Adressen zulässt. Der Löser selbst verlässt in keinem dieser Fälle Ihre Hardware.

Warum stirbt meine HTTP-getriggerte Lösung nach etwa vier Minuten?

Weil 230 Sekunden die Obergrenze für die Antwort einer HTTP-getriggerten Funktion sind, und sie kommt vom Load Balancer und nicht von Functions. Kein Plan, kein Wert in host.json und keine Anwendungseinstellung hebt sie an. Wenn Sie die Antwort auf derselben Anfrage wollen, muss die Lösung deutlich innerhalb dieses Fensters fertig sein, und das bedeutet, das reCAPTCHA-Polling-Timeout des Clients vom Standardwert 300 zu senken und hinzunehmen, dass langsame Lösungen scheitern. Die bessere Antwort ist, die Arbeit an eine Queue-getriggerte Funktion zu übergeben und sofort zurückzukehren.

Brauche ich dafür Durable Functions?

Nur wenn der Aufrufer das Ergebnis pollen muss. Durable Functions liefert Ihnen das asynchrone HTTP-Muster mit eingebautem Status-Endpunkt, und das lohnt sich, wenn ein Browser oder ein Partnersystem auf das Ergebnis wartet. Ist das Lösen ein Schritt Ihrer eigenen Pipeline, ist eine Storage Queue einfacher und verschafft Ihnen denselben Ausweg aus der 230-Sekunden-Grenze. Halten Sie das Lösen und alles, was das Token verbraucht, in jedem Fall im selben Aufruf, denn das Token läuft schnell ab, und eine Orchestrierungsgrenze ist ein guter Ort, um dieses Rennen zu verlieren.

Warum lässt sich mein catch-Block nicht kompilieren?

Weil der Client eine TimeoutException und eine ValidationException definiert, deren Kurznamen es auch in System gibt, und eine Funktionsdatei hat fast immer beide Namespaces im Gültigkeitsbereich. Schreiben Sie CapSkip.TimeoutException und CapSkip.ValidationException voll aus, oder fangen Sie den Basistyp CapSkipError ab und verzweigen Sie darin. Die anderen beiden, NetworkException und ApiException, kollidieren nicht und lassen sich über ihre Kurznamen abfangen.

Die Kurzfassung

Lösen Sie nicht in einem HTTP-Trigger. Die Anfrage wird nach 230 Sekunden vom Load Balancer abgeschnitten, egal was functionTimeout sagt, und das reCAPTCHA-Timeout des Clients ist standardmäßig länger als das. Nehmen Sie den Auftrag an, schreiben Sie eine Queue-Nachricht, antworten Sie sofort und lösen Sie in der Queue-getriggerten Funktion. Ziehen Sie die Queue-Retries zeitlich auseinander und fangen Sie die Parameterfehler ab, sonst verbrennt ein falscher sitekey fünf Versuche an einem Problem, das kein Retry beheben kann. Stellen Sie CapSkip auf den Server-Modus um und legen Sie seine Adresse in die Anwendungseinstellungen, denn Loopback in einer Function App ist die Instanz von Azure und nicht Ihre.

Eines sollten Sie abwägen, bevor Sie eine Batch-Größe festlegen: Captcha-Umgehung mit CapSkip läuft auf einer Maschine, die Ihnen bereits gehört, sodass die Obergrenze für gleichzeitige Lösungen davon abhängt, was diese Maschine tragen kann, und nicht davon, was die Monatsrechnung erlaubt.