Cara Memecahkan CAPTCHA di Inngest Tanpa Memecahkan Dua Kali

Pemecahan captcha di Inngest harus berada di dalam satu pemanggilan step.run, dan tokennya harus dipakai di dalam pemanggilan yang sama itu. Inngest mengeksekusi ulang function Anda dari atas pada setiap batas step, jadi apa pun yang ada di luar sebuah step akan berjalan lagi setiap kali. Dan step yang sudah selesai akan dimemoisasi, jadi token yang dipecahkan di step satu akan diputar ulang tanpa perubahan di step empat, jauh setelah token itu kedaluwarsa. Kedua aturan itu lahir dari model eksekusinya, bukan dari pemecahnya.
Apa yang Anda butuhkan
- Sebuah aplikasi Inngest dengan serve endpoint, biasanya di /api/inngest, dan Inngest Dev Server untuk menjalankannya secara lokal.
- CapSkip yang berjalan di sebuah mesin Windows, dengan client Node terinstal di aplikasi yang melayani function Anda.
- sitekey dan URL halaman, yang datang lewat payload event alih-alih ditanam di dalam function.
- Mode Server setiap kali aplikasi di-deploy di tempat selain mesin pemecah itu sendiri, yang berarti sebagian besar deployment. Itu hanya satu pengaturan di bagian pengaturan koneksi.
# npm install capskip npm install inngest capskip
Langkah 1: mengapa pemecahan harus ada di dalam sebuah step
Inngest tidak menjalankan function Anda sekali lalu menelusurinya sampai bawah. Ia menjalankan function, berhenti di step pertama, mencatat hasilnya, lalu memanggil function itu lagi dari atas dengan state dari eksekusi sebelumnya terlampir. Dokumentasinya sendiri menjelaskan lintasan kedua itu dengan gamblang: kode step tersebut tidak dieksekusi, dan sebagai gantinya SDK menyisipkan hasilnya ke dalam nilai balik step.run.
Itulah keseluruhan modelnya, dan ada satu konsekuensi yang lebih penting dari yang lain. Kode yang berada di luar sebuah step tidak dimemoisasi, jadi ia berjalan pada setiap invocation. Function dengan empat step memanggil handler Anda empat kali, jadi pemecahan yang ditulis di atas step-step itu berjalan empat kali untuk satu kali run function.
// 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 menyatakan aturannya secara langsung: setiap logika non-deterministik, seperti panggilan database atau panggilan API, harus ditempatkan di dalam pemanggilan step.run. Pemecahan adalah panggilan API, jadi tempatnya di dalamnya. Dengan pemecah berbasis kuota, kesalahan itu muncul sebagai tagihan. Dengan pemecah lokal, kesalahan itu muncul sebagai pekerjaan empat kali lipat dan empat token, tiga di antaranya terbuang.
Langkah 2: pecahkan dan kirimkan di step yang sama
Aturan kedua kurang kentara dan baru menggigit belakangan. Begitu sebuah step selesai, nilai baliknya disimpan dan diputar ulang pada setiap invocation berikutnya. Semua data yang dikembalikan dari step.run diserialisasi sebagai JSON, dan id step itulah yang menjadi kunci memoisasi state-nya.
Jadi token yang dikembalikan dari step pemecahan adalah sebuah string tersimpan. Ia kembali identik pada invocation berikutnya dan berikutnya lagi, dan saat itu umurnya bisa sudah beberapa menit. Token reCAPTCHA berlaku sekitar dua menit. Apa pun yang berada di antara pemecahan dan pengiriman memakan jendela waktu itu: sebuah sleep, fetch yang lambat, atau step yang mengulang beberapa kali dengan backoff. Salah satunya saja bisa membuat jendela itu habis.
// 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));Satukan keduanya. Satu step memecahkan dan mengirimkan, lalu mengembalikan hanya apa yang dibutuhkan sisa function-nya, yang hampir tidak pernah berupa token itu sendiri.
// 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 };
});Jendela kedaluwarsa itu sama dengan yang menjebak orang saat merangkai job antrean, dan layak dibaca sekali: berapa lama token reCAPTCHA bertahan.
Langkah 3: retry, dan error mana yang layak mendapatkannya
Inngest mengulang sebuah function atau step empat kali di luar percobaan pertama, dan setiap step.run punya penghitung retry sendiri yang independen. Pengulangan memakai exponential backoff dengan jitter. Anda bisa menyetel opsi retries dari nol sampai dua puluh.
Untuk step gabungan yang memecahkan sekaligus mengirim, nilai default itu sudah hampir pas, karena step yang diulang menjalankan ulang kodenya dan karena itu memecahkan lagi. Tidak ada token basi yang diwariskan. Yang layak disetel adalah kegagalan mana yang memang pantas diulang.
| Exception yang mana | Apa artinya | Layak diulang? |
|---|---|---|
| NetworkException | CapSkip tidak terjangkau, atau sedang mulai ulang | Ya. Untuk inilah retry ada |
| TimeoutException | Polling melewati batas atas milik client sendiri | Sekali, mungkin. Jarang layak empat kali |
| ApiException | API mengembalikan sebuah kode error | Tergantung kode errornya. Biasanya tidak |
| ValidationException | Parameternya salah dan akan tetap salah | Tidak. Lemparkan 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 melewati sisa retry dan menggagalkan step tempat ia dilemparkan, dan itulah yang Anda inginkan untuk request yang memang salah bentuk, bukan sekadar sedang sial. Jika pemecah memberi tahu bahwa ia sedang sibuk dan bukan rusak, RetryAfterError memungkinkan Anda menentukan sendiri jedanya alih-alih mengikuti kurva default.
Langkah 4: function lengkapnya
Semua di atas, dalam satu file. Perhatikan di mana client dibuat: di luar handler, sehingga ia dibuat sekali per proses alih-alih sekali per invocation, dan ia tidak menyimpan state per run.
// 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;
}
);Pemanggilan itu adalah reCAPTCHA v2. Jenis lainnya berbentuk sama: oper invisible atau enterprise disetel ke 1, atau version disetel ke v3 dengan sebuah action, atau panggil turnstile atau geetest sebagai gantinya. Daftar lengkapnya ada di halaman pemecah CAPTCHA Node.js.
Langkah 5: di mana kode Anda sebenarnya berjalan, dan mode apa yang dibutuhkannya
Inngest berbeda dari kebanyakan platform otomatisasi terkelola dengan cara yang menentukan isi bagian ini. Function Anda tidak berjalan di infrastruktur Inngest. Inngest memanggil aplikasi Anda lewat HTTP di sebuah serve endpoint, biasanya /api/inngest, dan kode Anda dieksekusi di dalam aplikasi Anda sendiri. Jadi pertanyaan “bisakah ini menjangkau 127.0.0.1” sama sekali bukan soal Inngest, melainkan soal di mana Anda men-deploy.
| Di mana aplikasi di-deploy | Mode koneksi yang mana |
|---|---|
| Secara lokal, terhadap Dev Server, di mesin CapSkip | Mode Local. 127.0.0.1 memang benar |
| Di server Anda sendiri atau sebuah VM di jaringan Anda | Pakai mode Server dengan alamat LAN pemecah CAPTCHA |
| Di host serverless seperti Vercel atau Lambda | Pakai mode Server dengan IP publik statis dan sebuah aturan firewall |
| Di dalam container di samping aplikasi, pemecah di tempat lain | Mode Server. Loopback di dalam container adalah container itu sendiri |
Ada dua mode koneksi. Local terikat ke 127.0.0.1 dan hanya melayani perangkat itu. Server terikat ke alamat jaringan atau IP publik Anda, sehingga mesin lain, sebuah container host atau serverless function bisa menjangkau mesin Windows yang sama lewat API. Keduanya berada di bagian pengaturan koneksi, dan mode Server hanya mengubah di alamat mana pemecah mendengarkan. Perangkat kerasnya tetap milik Anda dan pemecahannya tetap tanpa batas.
Tidak adanya penghitung pemakaian itulah yang membuat sebuah function yang menyala sepanjang hari masuk akal untuk dijalankan sama sekali.
Satu hal yang mudah tertukar: Inngest Cloud juga harus bisa menjangkau serve endpoint Anda, dan itu urusan jaringan yang terpisah dari pemecah. Deployment yang sudah bisa dipanggil Inngest tidak otomatis menjadi deployment yang bisa memanggil LAN Anda.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Beberapa pemecahan tercatat untuk satu kali run function | Pemecahan berada di luar step.run, jadi ia berulang di tiap batas step | Pindahkan ke dalam sebuah step |
| Pengiriman gagal dengan token yang tadinya terpecahkan dengan baik | Token yang dimemoisasi diputar ulang setelah kedaluwarsa | Pecahkan dan kirimkan di step yang sama |
| Sebuah step mengulang empat kali dan gagal dengan cara yang sama | Error parameter diperlakukan sebagai gangguan sementara | Lemparkan NonRetriableError untuk ValidationException |
| NetworkException di setiap run setelah deploy | Aplikasi berpindah dari mesin pemecah | Mode Server, dan setel CAPSKIP_HOST di deployment |
| Hasil sebuah step berubah bentuk antar deploy | Output step diserialisasi sebagai JSON dan dicocokkan berdasarkan id | Ganti nama id step ketika nilai baliknya berubah |
| CAPCHA_NOT_READY muncul dari polling buatan sendiri | Hasilnya dibaca sebelum selesai | Biarkan client yang melakukan polling. Ia melambat dengan sendirinya |
| ERROR_WRONG_USER_KEY di dalam ApiException | CAPSKIP_API_KEY tidak disetel di environment deployment | Setel di tempat aplikasi berjalan, lalu deploy ulang |
Baris keenam itulah yang paling dulu ditemui orang ketika mereka menulis loop polling sendiri alih-alih memakai client. Ejaan pada respons itu bukan salah ketik dari pihak kami, karena API memang benar-benar mengembalikannya seperti itu. Penjelasan lengkapnya ada di panduan CAPCHA_NOT_READY.
FAQ
Bisakah saya mengembalikan token dari sebuah step dan memakainya nanti?
Bisa, dan itu akan berhasil saat pengujian lalu gagal di produksi. Nilainya disimpan dan diputar ulang pada setiap invocation berikutnya, jadi begitu ada sesuatu yang lambat berada di antara kedua step itu, Anda sedang mengirimkan token yang kedaluwarsa selagi function menunggu. Satukan pemecahan dan apa pun yang memakai tokennya dalam satu step, dan kembalikan hasilnya, bukan kredensialnya.
Apakah retry memecahkan CAPTCHA baru atau memakai ulang yang lama?
Yang baru. Step yang gagal tidak dimemoisasi, jadi retry menjalankan ulang kode di dalamnya dan itu termasuk pemanggilan pemecahan. Setiap step.run menyimpan penghitung retry sendiri yang independen, jadi satu step yang rewel tidak menghabiskan jatah step lain. Justru inilah sebabnya step gabungan aman untuk diulang sedangkan step yang terpisah tidak.
Aplikasi saya ada di Vercel. Apakah ia masih bisa menjangkau pemecah di meja saya?
Ya, dengan mode Server. Function-nya berjalan di sandbox Vercel, jadi loopback di sana adalah sandbox itu sendiri, bukan mesin Anda. Ikat CapSkip ke IP publik Anda di bagian pengaturan koneksi, pasang aturan firewall di depannya yang hanya mengizinkan apa yang Anda harapkan, dan setel CAPSKIP_HOST di environment proyek. IP publik statis disarankan supaya alamatnya tidak berpindah tanpa Anda sadari.
Apa bedanya dengan melakukannya di Temporal?
Keduanya adalah durable execution dan keduanya berakhir pada aturan yang sama lewat jalur yang berbeda. Temporal memutar ulang sebuah workflow dari riwayat event-nya di dalam worker yang Anda jalankan, jadi aturannya lahir dari determinisme. Inngest memanggil ulang endpoint HTTP Anda dan menyisipkan hasil step yang dimemoisasi, jadi aturannya lahir dari pemutaran ulang ditambah token yang menua. Versi Temporal-nya dibahas tuntas di panduan workflow Temporal.
Versi singkatnya
Taruh pemecahan di dalam step.run, jangan pernah di atasnya, karena semua yang ada di luar sebuah step akan berjalan lagi di setiap batas step. Taruh pemecahan dan hal yang memakai tokennya di step yang sama, karena step yang sudah selesai akan dimemoisasi dan diputar ulang sedangkan token hanya hidup sekitar dua menit. Biarkan nilai retry default apa adanya, tetapi lemparkan NonRetriableError untuk error parameter. Setel CAPSKIP_HOST dari environment dan jalankan CapSkip dalam mode Server di mana pun aplikasi tidak berada di mesin pemecah itu sendiri.
- Endpoint mentah di balik klien didokumentasikan di dokumentasi API CapSkip.
- Tantangan kotak centangnya sendiri dijelaskan di halaman pemecah reCAPTCHA v2.
Perlu ditimbang sebelum Anda menetapkan batas concurrency pada function: CapSkip adalah alat bypass captcha yang berjalan di perangkat keras yang sudah Anda miliki, jadi satu-satunya batas berapa banyak run yang memecahkan sekaligus adalah mesin itu, bukan saldo.
