Cara Memecahkan CAPTCHA di Azure Function (C# Isolated)

Pemecahan captcha di Azure Functions rusak karena alasan yang tidak pernah ditunjukkan oleh kode Anda. Fungsi dengan trigger HTTP hanya punya 230 detik untuk merespons sebuah request, apa pun timeout yang Anda konfigurasikan, karena batas itu berasal dari load balancer di depan platform. Timeout polling reCAPTCHA di client CapSkip defaultnya 300 detik. Jadi pemecahan yang lambat dipotong oleh Azure, bukan oleh fungsi Anda, dan log hanya memperlihatkan request yang begitu saja berakhir. Perbaikannya adalah berhenti memecahkan CAPTCHA di atas request HTTP sama sekali. Hal lain yang harus benar adalah loopback, karena function app berjalan di mesin milik Azure dan bukan di mesin Anda.
Apa yang Anda butuhkan
- Sebuah function app .NET isolated worker. Dukungan untuk model in-process berakhir pada 10 November 2026, jadi isolated worker adalah model yang layak dijadikan fondasi.
- CapSkip yang berjalan di mesin Windows, dalam mode Server, di alamat yang bisa dijangkau function app.
- Sebuah storage account, karena pola di bawah memindahkan pemecahan CAPTCHA ke fungsi dengan trigger queue.
- sitekey dan URL halaman yang datang lewat pesan queue, bukan ditulis langsung di kode, sehingga satu fungsi melayani semua form.
# dotnet add package CapSkip dotnet add package CapSkip dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues
Langkah 1: loopback di dalam function app berarti function app itu sendiri
Perlu diputuskan sebelum hal lain, karena ini menentukan apakah sisanya bisa berjalan. Fungsi Anda berjalan di instance yang disediakan Azure, jadi 127.0.0.1 di dalamnya adalah instance itu sendiri. Tidak ada apa pun yang mendengarkan di port 8080 di sana, dan kegagalannya muncul sebagai NetworkException pada pemecahan pertama setelah deploy, padahal kode yang sama berjalan sempurna di tooling lokal.
Ada dua mode koneksi. Mode Local mengikat ke 127.0.0.1 dan hanya melayani perangkat itu, dan itu memang tepat ketika otomatisasi dan solver Anda berada di satu mesin. Mode Server mengikat ke alamat jaringan atau IP publik Anda, sehingga mesin lain, sebuah VPS, atau platform hosting bisa menjangkau mesin Windows yang sama lewat API. Mode Server hanya mengubah alamat mana yang didengarkan solver. Perangkat kerasnya tetap milik Anda dan tetap tanpa kuota. Kedua mode itu ada di pengaturan koneksi.
| Bagaimana Anda menjalankan fungsi itu | Mode koneksi yang mana |
|---|---|
| Tooling lokal, di mesin CapSkip | Mode Local. 127.0.0.1 memang benar |
| Sudah di-deploy, dengan solver di jaringan yang bisa dijangkau rute Azure | Pakai mode Server dengan alamat privat itu |
| Sudah di-deploy, dengan solver dijangkau lewat internet | Pakai mode Server dengan IP publik statis dan sebuah aturan firewall |
Ada dua fitur Azure yang perlu Anda ketahui ketika solver berada di jaringan yang bisa dijangkau rute Azure. Virtual network integration untuk lalu lintas keluar tersedia pada paket Flex Consumption, Premium dan Dedicated, dan sama sekali tidak tersedia pada paket Consumption yang lama. Hybrid Connections, yang dibuat untuk menjangkau layanan yang tetap berada di jaringan Anda sendiri, tersedia pada paket Premium dan Dedicated untuk aplikasi yang berjalan di Windows. Bagaimanapun caranya, solver tetap berada di perangkat keras Anda; hanya rutenya yang berubah.
Simpan alamat itu di pengaturan aplikasi, bukan di source code, karena tooling lokal dan aplikasi yang sudah di-deploy menginginkan nilai yang berbeda. Client tidak membaca environment variable apa pun dengan sendirinya, jadi baca CAPSKIP_HOST di kode Anda lalu oper nilainya ke client, seperti yang dilakukan fungsi di bawah.
// 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();Langkah 2: tembok 230 detik, dan kenapa itu bukan timeout Anda
Ini bagian yang membuang satu sore penuh, karena setiap angka yang bisa Anda lihat di portal lebih besar daripada angka yang sebenarnya mematikan request itu.
Microsoft mendokumentasikannya dengan gamblang: terlepas dari pengaturan timeout function app, 230 detik adalah waktu maksimum yang bisa dipakai fungsi dengan trigger HTTP untuk merespons sebuah request, dan batas itu ada karena idle timeout default pada Azure Load Balancer. Anda tidak bisa menaikkannya lewat host.json, lewat pengaturan aplikasi, atau dengan mengganti paket.
Sekarang sandingkan dengan angka milik client itu sendiri. Timeout polling untuk reCAPTCHA, Turnstile dan GeeTest defaultnya 300 detik, dan timeout gambar 120 detik. Jadi pemecahan gambar muat di dalam tembok itu dengan nyaman, sementara pemecahan reCAPTCHA boleh berjalan 70 detik melewatinya. Sebagian besar pemecahan selesai jauh sebelum kedua angka itu, dan justru itulah sebabnya hal ini lolos ke produksi lalu gagal pada ekor yang lambat.
| Limit | Nilai | Bisakah Anda mengubahnya? |
|---|---|---|
| Respons HTTP, paket apa pun | 230 detik | Tidak |
| Timeout polling reCAPTCHA di client | 300 detik | Bisa, lewat constructor |
| Timeout polling gambar di client | 120 detik | Bisa, lewat constructor |
Menurunkan timeout polling reCAPTCHA ke bawah 230 detik tetap layak dilakukan, karena client yang menyerah lebih dulu menghasilkan CapSkip.TimeoutException yang bisa Anda catat di log, bukan request yang lenyap begitu saja. Tapi itu bukan perbaikan yang sebenarnya. Perbaikan yang sebenarnya adalah yang diberikan dokumentasi Azure: gunakan pola async Durable Functions, atau tunda pekerjaan aslinya dan kembalikan respons seketika. Dalam praktiknya itu berarti trigger HTTP menerima job, menulis sebuah pesan, lalu langsung kembali.
[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;
}Langkah 3: timeout function app adalah batas yang terpisah
Begitu pemecahan CAPTCHA lepas dari request HTTP, timeout yang penting adalah yang ada di host.json, dan nilainya berbeda menurut paket. Nilai defaultnya murah hati di mana-mana kecuali pada paket Consumption yang lama, satu-satunya paket di mana pemecahan reCAPTCHA yang lambat benar-benar bisa kehabisan ruang.
| Paket hosting | Timeout default | Timeout maksimum |
|---|---|---|
| Paket Flex Consumption | 30 menit | Tidak ada maksimum yang dipaksakan |
| Paket Premium | 30 menit | Tidak ada maksimum yang dipaksakan |
| Paket Dedicated | 30 menit | Tidak ada maksimum yang dipaksakan, dengan Always On |
| Paket Consumption, versi lama | 5 menit | 10 menit |
Lima menit itu tepat 300 detik, jadi paket lama tidak menutupi timeout reCAPTCHA pada nilai defaultnya: fungsinya mati tepat pada saat client seharusnya sudah menyerah. Naikkan kalau Anda masih memakai paket itu, dan jaga agar timeout milik client sendiri tetap di bawah berapa pun nilai yang Anda setel.
{
"version": "2.0",
"functionTimeout": "00:10:00"
}Langkah 4: berapa kali sebuah pesan queue dipecahkan ulang
Memindahkan pemecahan CAPTCHA ke queue memberi Anda ruang, dan ia membawa perilaku pengulangannya sendiri yang membuang waktu sungguhan kalau Anda biarkan begitu saja.
Ketika fungsi dengan trigger queue gagal, Azure Functions menjalankan fungsi itu sampai lima kali untuk pesan tersebut, termasuk percobaan pertama. Jika kelimanya gagal, runtime menulis pesan itu ke queue yang dinamai seperti aslinya dengan akhiran poison. Itu berarti lima pemecahan untuk satu CAPTCHA kalau kegagalannya adalah sesuatu yang tidak akan pernah berhasil, misalnya sitekey yang bukan milik halaman itu.
Keadaannya lebih sempit daripada kelihatannya. Visibility timeout di host.json defaultnya nol, yang berarti pesan yang gagal langsung muncul lagi, sehingga kelima percobaan itu bisa terjadi berturut-turut dalam hitungan detik. Setel ke nilai yang memberi waktu bagi masalah sesaat untuk pulih, dan baca dequeue count di dalam fungsi supaya pesan yang sedang menjalani percobaan terakhirnya bisa ditangani secara berbeda.
Concurrency adalah separuh sisanya. Secara default trigger mengambil satu batch berisi 16 pesan, lalu mengambil 16 lagi begitu jumlah yang masih berjalan turun ke 8. Yang 8 itu masih berjalan ketika batch baru dimulai, sehingga satu instance bisa menjalankan 24 pemecahan sekaligus untuk satu fungsi. Ketika aplikasi melakukan scale out, angka itu dikalikan dengan jumlah instance. Pada layanan yang menagih per pemecahan, Anda akan membatasinya untuk melindungi saldo. Di sini ini soal kapasitas satu mesin Windows, tapi 24 pemecahan serentak per instance tetap keputusan yang layak diambil dengan sengaja, bukan sekadar diwarisi.
Contoh di bawah ini sengaja berada di bawah kedua nilai default itu: tiga attempts, bukan lima, dan batch berisi delapan, bukan enam belas.
{
"version": "2.0",
"extensions": {
"queues": {
"batchSize": 8,
"newBatchThreshold": 4,
"visibilityTimeout": "00:00:30",
"maxDequeueCount": 3
}
}
}Contoh lengkap yang berfungsi
Bagian yang dipicu queue, dengan pemecahan CAPTCHA dan pengirimannya di dalam satu invocation yang sama.
// 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);
}
}
}Panggilan itu adalah reCAPTCHA v2. Tipe lainnya berbentuk sama: oper sebuah dictionary options dengan invisible atau enterprise disetel ke 1, atau version disetel ke v3 beserta sebuah action, atau panggil TurnstileAsync atau GeetestAsync sebagai gantinya. Seluruh permukaannya ada di halaman pemecah CAPTCHA C#.
Turnstile pada halaman challenge adalah satu-satunya pengecualian yang perlu Anda ketahui, karena ia butuh dua nilai tambahan dari halaman itu, ditambah user agent yang dipakai solver. Yang satu itu punya panduan tersendiri.
Menelan error parameter alih-alih melemparnya ulang itu disengaja. Exception yang dilempar adalah yang memulai siklus lima percobaan, dan tidak ada yang bisa dilakukan retry terhadap sitekey yang tidak cocok dengan halamannya.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Request HTTP berakhir sekitar empat menit tanpa error | Batas 230 detik dari load balancer, bukan timeout Anda | Kembalikan respons seketika dan pecahkan di trigger queue |
| Jalan dengan tooling lokal, NetworkException setelah di-deploy | Loopback di dalam function app adalah instance Azure itu | Pakai mode Server, dan setel CAPSKIP_HOST di pengaturan aplikasi |
| Lima kegagalan yang identik, lalu sebuah pesan di poison queue | Error yang tidak bisa di-retry terlempar keluar dari fungsi | Tangkap CapSkip.ValidationException dan catat saja di log |
| Lima percobaan habis dalam waktu kurang dari satu menit | Visibility timeout queue nilai defaultnya nol | Setel visibility timeout supaya retry diberi jarak |
| Build gagal karena TimeoutException yang ambigu | CapSkip dan System sama-sama mendefinisikan nama pendek itu | Tulis lengkap dengan namespace-nya, atau tangkap CapSkipError sebagai basisnya |
| ERROR_WRONG_USER_KEY di dalam ApiException | CAPSKIP_API_KEY belum disetel di aplikasi yang sudah di-deploy | Tambahkan ke pengaturan aplikasi, lalu restart aplikasinya |
| CAPCHA_NOT_READY dari polling buatan sendiri | Hasilnya dibaca sebelum selesai | Biarkan client yang melakukan polling. Ia melambat dengan sendirinya |
Respons terakhir itu memang ditulis persis seperti kelihatannya, dan huruf yang hilang bukan salah ketik dari pihak kami, karena API memang benar-benar mengembalikannya seperti itu. Penjelasan lengkapnya ada di panduan CAPCHA_NOT_READY.
FAQ
Bisakah function app di Azure menjangkau solver di jaringan saya sendiri?
Bisa. Ubah CapSkip ke mode Server di pengaturan koneksi supaya ia mendengarkan alamat jaringan, bukan loopback, lalu arahkan CAPSKIP_HOST ke alamat itu di pengaturan aplikasi. Untuk rute privat, virtual network integration tersedia pada paket Flex Consumption, Premium dan Dedicated, dan Hybrid Connections pada Premium dan Dedicated untuk aplikasi yang berjalan di Windows. Kalau Anda memilih lewat internet, gunakan IP publik statis dengan aturan firewall yang hanya mengizinkan alamat yang Anda harapkan. Solvernya sendiri tidak pernah meninggalkan perangkat keras Anda dalam semua cara ini.
Kenapa pemecahan CAPTCHA saya yang dipicu HTTP mati sekitar empat menit?
Karena 230 detik adalah langit-langit bagi fungsi dengan trigger HTTP untuk merespons, dan angka itu datang dari load balancer, bukan dari Functions. Tidak ada paket, nilai host.json, atau pengaturan aplikasi yang bisa menaikkannya. Kalau Anda ingin jawabannya di request yang sama, pemecahan harus selesai jauh di dalam jendela itu, yang berarti menurunkan timeout polling reCAPTCHA di client dari nilai defaultnya 300 dan menerima bahwa pemecahan yang lambat akan gagal. Jawaban yang lebih baik adalah menyerahkan pekerjaan itu ke fungsi dengan trigger queue lalu langsung kembali.
Apakah saya butuh Durable Functions untuk ini?
Hanya kalau pemanggilnya harus melakukan polling untuk mendapatkan hasilnya. Durable Functions memberi Anda pola HTTP async lengkap dengan endpoint status bawaan, dan itu sepadan ketika sebuah browser atau sistem mitra sedang menunggu hasilnya. Kalau pemecahan CAPTCHA hanya satu langkah di pipeline Anda sendiri, storage queue lebih sederhana dan memberi jalan keluar yang sama dari batas 230 detik. Bagaimanapun pilihannya, simpan pemecahan dan apa pun yang memakai tokennya di dalam satu invocation yang sama, karena token cepat kedaluwarsa dan batas orkestrasi adalah tempat yang bagus untuk kalah dalam perlombaan itu.
Kenapa blok catch saya tidak mau dikompilasi?
Karena client mendefinisikan TimeoutException dan ValidationException yang nama pendeknya juga ada di System, dan sebuah file fungsi hampir selalu punya kedua namespace itu dalam cakupannya. Tulis CapSkip.TimeoutException dan CapSkip.ValidationException secara lengkap, atau tangkap CapSkipError sebagai basisnya lalu bercabang di dalamnya. Dua yang lain, NetworkException dan ApiException, tidak berbenturan dan bisa ditangkap dengan nama pendeknya.
Versi singkatnya
Jangan memecahkan CAPTCHA di trigger HTTP. Request dipotong pada 230 detik oleh load balancer, apa pun isi functionTimeout, dan timeout reCAPTCHA di client secara default lebih panjang dari itu. Terima job-nya, tulis sebuah pesan queue, kembalikan respons seketika, lalu pecahkan di fungsi dengan trigger queue. Beri jarak antar retry queue dan tangkap error parameter, atau satu sitekey yang salah akan menghanguskan lima attempts untuk masalah yang tidak bisa diperbaiki retry mana pun. Ubah CapSkip ke mode Server dan simpan alamatnya di pengaturan aplikasi, karena loopback di dalam function app adalah instance milik Azure dan bukan milik Anda.
- Endpoint mentah di balik klien didokumentasikan di dokumentasi API CapSkip.
- Tantangan kotak centangnya sendiri dijelaskan di halaman pemecah reCAPTCHA v2.
Satu hal yang perlu ditimbang sebelum Anda memilih ukuran batch: bypass captcha dengan CapSkip berjalan di mesin yang sudah Anda miliki, sehingga batas atas pemecahan serentak ditentukan oleh apa yang sanggup dipikul mesin itu, bukan oleh apa yang diizinkan tagihan bulanan.
