Cara Memecahkan CAPTCHA di Netlify Functions (SDK Node.js)

Pemecahan CAPTCHA di Netlify Functions tidak bisa berjalan di fungsi sinkron yang menjawab request. Netlify menghentikan fungsi itu pada 60 detik dan tidak mengizinkan Anda menaikkan batasnya, sedangkan pemecahan reCAPTCHA bisa memakan waktu beberapa menit. Sebagai gantinya, pecahkan di fungsi latar belakang (background function), yang mendapat 15 menit, biarkan fungsi itu mengirim form sendiri, dan catat hasilnya di Netlify Blobs agar fungsi kedua bisa melaporkannya. Arahkan SDK Node.js CapSkip ke pemecah CAPTCHA Anda dalam mode Server, karena 127.0.0.1 di dalam fungsi Netlify adalah mesin milik Netlify, bukan mesin Anda. Lalu tangani percobaan ulang: fungsi latar belakang yang melempar exception akan dijalankan lagi, dan fungsi yang ceroboh akan mengirim form dua kali.
Apa yang Anda butuhkan
- CapSkip yang berjalan di mesin Windows yang Anda kendalikan, dalam mode Server, dengan IP publik statis dan port pemecah CAPTCHA yang bisa dijangkau dari internet. Mode Server ada di pengaturan koneksi, dan Langkah 1 membahas sisanya.
- Situs Netlify dengan fungsi di netlify/functions, yang di-build dengan Node.js 22.12 atau lebih baru, versi yang dibutuhkan @netlify/blobs. Netlify menjalankan fungsi pada versi Node.js yang dipakai build Anda. Fungsi latar belakang tersedia di setiap paket berbasis kredit, termasuk Free; paket legacy berbeda-beda, jadi periksa paket Anda.
- Paket capskip versi 1.3.0 atau lebih baru, ditambah @netlify/blobs untuk catatan job dan cheerio untuk membaca halaman. Di dalam fungsi, Blobs tidak perlu disiapkan: Netlify mengisi situs dan token untuk Anda.
- URL halaman yang memiliki CAPTCHA. Contoh kodenya memecahkan reCAPTCHA v2, dan bentuk yang sama berlaku untuk setiap tipe yang ditangani CapSkip.
# npm install capskip @netlify/blobs cheerio npm install capskip @netlify/blobs cheerio
Mengapa fungsi sinkron tidak bisa melakukannya
Netlify memberi setiap jenis fungsi batas eksekusi yang tetap, seperti tercantum di dokumentasi konfigurasi fungsi miliknya. Tidak satu pun dari ketiganya bisa diubah:
| Jenis fungsi | Batas eksekusi | Tugasnya dalam susunan ini |
|---|---|---|
| Sinkron | 60 detik | Melaporkan hasil job |
| Terjadwal | 30 detik | Memulai job berdasarkan timer |
| Latar belakang | 15 menit | Memecahkan, lalu mengirim form |
Sekarang bandingkan dengan pemecahan CAPTCHA di Netlify Functions. SDK menunggu hingga recaptchaTimeout, 300 detik secara default, untuk jawaban reCAPTCHA, dan CapSkip sendiri mengizinkan sebuah tugas menunggu 250 detik untuk thread kosong lalu menghabiskan 250 detik lagi untuk memecahkan. Pemecahan yang memakan 20 detik pada sore yang sepi bisa memakan 90 detik ketika semua thread sibuk atau proxy lambat. Ketika fungsi sinkron mencapai 60 detik, Netlify mengakhirinya. CapSkip tidak tahu hal itu, jadi ia tetap mengerjakan job sampai selesai dan tidak ada yang pernah mengambil jawabannya.
context.waitUntil tampak seperti jalan keluar, karena ia membuat fungsi tetap berjalan setelah respons terkirim. Ternyata bukan. Dokumentasi Netlify menyebutkan bahwa fungsi tetap hanya bisa berjalan sampai batas eksekusinya, termasuk pekerjaan async, jadi pemecahan yang diserahkan ke waitUntil tetap mati pada 60 detik. Respons streaming juga berbagi batas 60 detik yang sama.
Fungsi latar belakang langsung menjawab pemanggil dengan 202 dan terus berjalan hingga 15 menit. Harganya ada pada 202 itu: tidak ada yang menerima nilai kembalian fungsi. Selain itu, token reCAPTCHA kedaluwarsa sekitar dua menit setelah diterbitkan, jadi token itu tidak bisa diam menunggu pemanggil datang mengambilnya. Fungsi latar belakang harus memakai token itu sendiri, dengan mengirim form, lalu meninggalkan catatan tentang apa yang terjadi.
Langkah 1: arahkan SDK ke pemecah CAPTCHA Anda dalam mode Server
Di dalam fungsi Netlify, 127.0.0.1 adalah sandbox milik fungsi itu sendiri. Klien yang dibangun dengan pengaturan default tidak terhubung ke apa pun di sana dan melempar NetworkException dengan ECONNREFUSED. Alihkan CapSkip ke mode Server agar ia mendengarkan di IP publik Anda, teruskan port ke mesin Windows jika mesin itu berada di belakang router, dan izinkan port tersebut melalui Windows Firewall.
Lalu tentukan siapa yang boleh terhubung. Secara default, alamat yang dipakai fungsi Netlify untuk terhubung berubah seiring Netlify melakukan scaling, sehingga aturan firewall tidak bisa mencantumkannya. Private Connectivity dari Netlify memberi fungsi sekumpulan IP tetap yang bisa Anda izinkan, tetapi fitur itu adalah add-on untuk paket Enterprise. Tanpa fitur itu, kuncinya adalah pengaturan API Key Validation di CapSkip: nyalakan, tambahkan key untuk situs ini, dan berikan sebagai apiKey. Setiap request SDK membawa key tersebut, melalui HTTP biasa seperti bagian lain dari pemanggilan, jadi beri fungsi itu key tersendiri yang bisa Anda hapus tanpa merusak hal lain.
Simpan alamat dan key itu sebagai variabel lingkungan di Netlify, di Project configuration, lalu Environment variables, dengan scope yang mencakup Functions. Dua aturan Netlify sering menjebak orang di sini. Variabel yang dideklarasikan di netlify.toml sama sekali tidak pernah sampai ke fungsi. Selain itu, setiap deploy menyimpan nilai yang disetel saat deploy itu di-build, jadi CAPSKIP_HOST yang baru tidak berpengaruh apa pun sampai Anda melakukan deploy lagi.
import { CapSkip } from "capskip";
// The SDK does not read these by itself, so pass them in.
if (!process.env.CAPSKIP_HOST) throw new Error("CAPSKIP_HOST is not set");
const solver = new CapSkip({
apiKey: process.env.CAPSKIP_API_KEY, // a key from API Key Validation
host: process.env.CAPSKIP_HOST, // your public IP, Server mode
port: Number(process.env.CAPSKIP_PORT ?? 8080),
});Pertahankan pemeriksaan itu. Jika variabelnya tidak ada, host bernilai undefined, dan SDK kembali ke default 127.0.0.1 tanpa peringatan apa pun, yang langsung mengembalikan Anda ke ECONNREFUSED. Di contoh lengkap, pemeriksaan itu berada di dalam blok try milik pemecahan, sehingga variabel yang hilang masuk ke catatan job alih-alih menghentikan fungsi sebelum ia sempat menulisnya.
Langkah 2: pecahkan dan kirim di fungsi latar belakang
Menyetel background ke true di config fungsi sudah cukup untuk menjadikannya fungsi latar belakang. Fungsi ini memeriksa shared secret, karena URL-nya publik dan setiap request ke sana membuat pemecah CAPTCHA Anda bekerja. Lalu fungsi ini mengambil halaman, membaca sitekey, dan memecahkan:
// netlify/functions/solve-signup.mjs
import { getStore } from "@netlify/blobs";
import * as cheerio from "cheerio";
// solver from Step 1; PAGE_URL is the page with the CAPTCHA.
export default async (req) => {
if (req.headers.get("x-job-secret") !== process.env.JOB_SECRET) return;
const { jobId, email } = await req.json();
const jobs = getStore({ name: "captcha-jobs", consistency: "strong" });
const page = await fetch(PAGE_URL);
const $ = cheerio.load(await page.text());
const result = await solver.recaptcha($(".g-recaptcha").attr("data-sitekey"), PAGE_URL);
// Then claim the job and post the form, below.
};
export const config = { path: "/api/solve-signup", method: "POST", background: true };Jawaban untuk reCAPTCHA ada di result.code. Pakai segera: dengan masa berlaku dua menit, eksekusi yang memecahkan haruslah eksekusi yang mengirim form.
Sekarang soal percobaan ulang. Ketika fungsi latar belakang berakhir dengan error, Netlify menjalankannya lagi satu menit kemudian, dan jika itu juga gagal, sekali lagi dua menit setelahnya. Sebelum form dikirim, percobaan ulang adalah persis yang Anda inginkan: halaman baru dan pemecahan baru. Setelah form dikirim, percobaan ulang berarti pendaftaran kedua. Jadi fungsi itu mengklaim job di Blobs sebelum mengirim form:
// Only one run can create this key, so only one run posts.
const claim = await jobs.setJSON(`${jobId}-posted`, { at: Date.now() }, { onlyIfNew: true });
if (!claim.modified) return;
const form = $("form").has(".g-recaptcha");
const body = new URLSearchParams(form.serializeArray().map((f) => [f.name, f.value]));
body.set("email", email);
body.set("g-recaptcha-response", result.code);
const cookie = page.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
const res = await fetch(new URL(form.attr("action") || PAGE_URL, PAGE_URL), {
method: "POST",
headers: { cookie, referer: PAGE_URL },
body,
});
await jobs.setJSON(jobId, { state: "done", status: res.status });Dengan onlyIfNew, penulisan hanya berhasil jika key itu belum ada, dan modified menunjukkan apakah eksekusi ini yang membuatnya. Dua eksekusi yang berlomba untuk job yang sama tidak mungkin sama-sama mendapat true, jadi hanya salah satunya yang mengirim form. Fungsi itu juga memeriksa key tersebut sebelum mulai, sehingga eksekusi ulang dari job yang sudah selesai langsung kembali tanpa menghabiskan pemecahan.
Kiriman itu sendiri membawa cookie halaman dan field milik form itu sendiri, termasuk token CSRF tersembunyi, dan itulah yang diperiksa sebagian besar form pendaftaran selain CAPTCHA. Kiriman itu juga mengirim halaman sebagai Referer, karena fetch tidak mengirimnya dan sebagian framework menolak form post HTTPS tanpa header itu. Begitu klaim sudah diambil, catat kegagalan apa pun di Blobs alih-alih melempar exception. Percobaan ulang akan berhenti di klaim, jadi melempar exception tidak menghasilkan apa-apa, dan catatan itu adalah satu-satunya tempat endpoint status Anda bisa menampilkan error. Contoh lengkap di bawah membungkus kedua fase dengan cara ini.
Langkah 3: mulai job dan baca hasilnya
Mulai job dari kode server-side mana pun dengan mengirim POST ke fungsi latar belakang. Pemanggil yang membuat id job, karena 202 tidak punya body untuk membawanya kembali:
const jobId = crypto.randomUUID();
const start = await fetch("https://YOUR_SITE.netlify.app/api/solve-signup", {
method: "POST",
headers: { "x-job-secret": process.env.JOB_SECRET },
body: JSON.stringify({ jobId, email: "YOUR_EMAIL" }),
});
console.log(start.status); // 202: accepted, not solved yetFungsi sinkron kecil melaporkan catatan itu. Ia berjalan dalam hitungan milidetik, jauh di bawah batas 60 detik:
// netlify/functions/job-status.mjs
import { getStore } from "@netlify/blobs";
export default async (req, context) => {
const jobs = getStore({ name: "captcha-jobs", consistency: "strong" });
const job = await jobs.get(context.params.id, { type: "json" });
if (!job) return new Response("unknown or not started", { status: 404 });
return Response.json(job);
};
export const config = { path: "/api/jobs/:id", method: "GET" };Lakukan polling setiap beberapa detik. Job tidak punya catatan sampai ia selesai atau sebuah percobaan gagal, jadi 404 berarti percobaan pertamanya masih berjalan, atau job itu tidak pernah dimulai karena secret-nya tidak cocok. Baca dengan strong consistency, seperti di sini. Secara default, Blobs bersifat eventually consistent: catatan baru langsung muncul, tetapi pembaruan bisa memakan waktu hingga 60 detik untuk mencapai setiap lokasi edge, cukup lama untuk menampilkan job yang sebenarnya sudah selesai sebagai masih dicoba ulang.
Untuk menjalankan job berdasarkan timer, gunakan fungsi terjadwal, tetapi perhatikan batasnya: 30 detik, separuh dari batas fungsi sinkron. Biarkan fungsi itu menyerahkan pekerjaan ke fungsi latar belakang dan selesai jauh di bawah satu detik:
// netlify/functions/nightly-signup.mjs
export default async () => {
const res = await fetch(`${process.env.URL}/api/solve-signup`, {
method: "POST",
headers: { "x-job-secret": process.env.JOB_SECRET },
body: JSON.stringify({ jobId: crypto.randomUUID(), email: "YOUR_EMAIL" }),
});
console.log("queued:", res.status); // 202 means accepted, not solved
};
export const config = { schedule: "@daily" };URL adalah salah satu variabel read-only yang diberikan Netlify kepada fungsi saat runtime: alamat utama situs Anda. Fungsi terjadwal hanya berjalan pada deploy yang sudah dipublikasikan, bukan pada Deploy Previews atau branch deploy.
Contoh lengkap yang berfungsi
// npm install capskip @netlify/blobs cheerio
// netlify/functions/solve-signup.mjs
import { getStore } from "@netlify/blobs";
import * as cheerio from "cheerio";
import { CapSkip } from "capskip";
const PAGE_URL = "https://example.com/signup";
export default async (req) => {
// Anyone can POST to this URL, so check a shared secret first.
if (req.headers.get("x-job-secret") !== process.env.JOB_SECRET) return;
const { jobId, email } = await req.json();
const jobs = getStore({ name: "captcha-jobs", consistency: "strong" });
if (await jobs.get(`${jobId}-posted`)) return; // already posted once
// Phase 1: fetch and solve. Throwing here is safe: Netlify runs
// the function again after one minute, then two minutes later.
let page, $, result;
try {
if (!process.env.CAPSKIP_HOST) throw new Error("CAPSKIP_HOST is not set");
const solver = new CapSkip({
apiKey: process.env.CAPSKIP_API_KEY,
host: process.env.CAPSKIP_HOST,
port: Number(process.env.CAPSKIP_PORT ?? 8080),
});
page = await fetch(PAGE_URL);
$ = cheerio.load(await page.text());
result = await solver.recaptcha($(".g-recaptcha").attr("data-sitekey"), PAGE_URL);
} catch (err) {
const prev = await jobs.get(jobId, { type: "json" });
const attempt = (prev?.attempt ?? 0) + 1;
// The first run plus two retries: after the third, nothing reruns.
const state = attempt < 3 ? "retrying" : "failed";
await jobs.setJSON(jobId, { state, attempt, error: String(err) });
throw err;
}
// Phase 2: post the form once. Claim the job first, so a rerun
// that reaches this line finds the claim taken and stops.
const claim = await jobs.setJSON(`${jobId}-posted`, { at: Date.now() }, { onlyIfNew: true });
if (!claim.modified) return;
try {
const form = $("form").has(".g-recaptcha");
const body = new URLSearchParams(form.serializeArray().map((f) => [f.name, f.value]));
body.set("email", email);
body.set("g-recaptcha-response", result.code);
const cookie = page.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
const res = await fetch(new URL(form.attr("action") || PAGE_URL, PAGE_URL), {
method: "POST",
headers: { cookie, referer: PAGE_URL },
body,
});
await jobs.setJSON(jobId, { state: "done", status: res.status });
} catch (err) {
// No rethrow: a retry could not post again, so record it here.
await jobs.setJSON(jobId, { state: "failed", error: String(err) });
}
};
export const config = { path: "/api/solve-signup", method: "POST", background: true };Setel JOB_SECRET, CAPSKIP_HOST, CAPSKIP_API_KEY dan, jika Anda mengubahnya, CAPSKIP_PORT di Netlify UI, lakukan deploy, lalu mulai job seperti di Langkah 3. Setiap opsi yang diterima pemanggilan reCAPTCHA, termasuk invisible dan Enterprise, berfungsi di sini tanpa perubahan; halaman pemecah reCAPTCHA v2 membahas apa yang dibutuhkan tipe ini.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| Request gagal setelah sekitar satu menit dan tidak ada form yang dikirim | Pemecahan berjalan di fungsi sinkron, atau di dalam waitUntil, yang berbagi batas 60 detik yang sama | Pindahkan pemecahan ke fungsi latar belakang |
| Catatan job yang menyebut CAPSKIP_HOST belum disetel, atau NetworkException dengan ECONNREFUSED 127.0.0.1:8080 jika Anda membuang pemeriksaan itu | CAPSKIP_HOST tidak ada di fungsi yang sudah di-deploy, sehingga SDK akan kembali ke loopback | Setel variabel itu dengan scope yang mencakup Functions, lalu deploy ulang |
| Berfungsi dengan netlify dev, gagal setelah di-deploy | Secara lokal, fungsi berjalan di mesin Anda sendiri, tempat loopback dan IP lokal Anda bisa menjangkau pemecah CAPTCHA | Gunakan mode Server dan IP publik Anda untuk situs yang di-deploy |
| Variabel dari netlify.toml bernilai undefined di dalam fungsi | Variabel yang dideklarasikan di netlify.toml tidak pernah sampai ke fungsi | Setel di Netlify UI, CLI, atau API sebagai gantinya |
| Fungsi masih memakai CAPSKIP_HOST yang lama | Sebuah deploy menyimpan nilai yang disetel saat deploy itu di-build | Deploy ulang setelah mengubah variabel |
| Koneksi yang hang, lalu NetworkException dengan ETIMEDOUT | Port tidak diteruskan, atau Windows Firewall memblokirnya | Teruskan port, izinkan melalui Windows Firewall, dan uji dari luar jaringan Anda |
| ApiException berisi ERROR_KEY_DOES_NOT_EXIST | API Key Validation aktif dan key tersebut tidak ada di daftar CapSkip, atau CAPSKIP_API_KEY tidak disetel dan SDK mengirim key default-nya | Tambahkan key di CapSkip dan setel variabelnya |
| Form terkirim dua kali | Error setelah pengiriman memicu percobaan ulang, dan tidak ada yang menghentikan eksekusi kedua | Klaim job dengan onlyIfNew sebelum mengirim form, seperti di Langkah 2 |
| Endpoint status terus mengembalikan 404 | Secret tidak cocok, sehingga fungsi kembali sebelum menulis apa pun | Setel JOB_SECRET yang sama di pemanggil dan di situs |
| Situs menolak token | Token berumur lebih dari sekitar dua menit, atau sudah dipakai | Kirim segera setelah memecahkan, satu token per kiriman |
FAQ
Bisakah saya menaikkan batas 60 detik untuk pemecahan CAPTCHA di Netlify Functions?
Tidak. Netlify mencantumkan batas fungsi sinkron, terjadwal, dan latar belakang sebagai batas tetap, dan waitUntil serta respons streaming tetap berada di dalam 60 detik itu. Batas 15 menit untuk fungsi latar belakang adalah batas panjang yang tersedia, dan itu mencakup waktu tunggu SDK selama 300 detik dengan sisa yang sangat lega.
Bisakah fungsi Netlify menjangkau CapSkip di PC rumah atau kantor saya?
Bisa, melalui mode Server. CapSkip mendengarkan di IP publik Anda, router Anda meneruskan port ke PC itu, dan fungsi terhubung melalui API HTTP yang sama yang akan dipakainya secara lokal. IP publik statis menjaga CAPSKIP_HOST tetap valid di antara deploy. Tanpa Private Connectivity, Anda tidak bisa mengizinkan Netlify berdasarkan alamat, jadi API Key Validation yang menjadi penjaga gerbangnya.
Apakah CapSkip menagih per pemecahan saat Netlify memanggilnya?
Tidak. Mode Server mengubah dari mana pemecah CAPTCHA bisa dijangkau, bukan siapa yang menjalankannya: ia tetap mesin Windows milik Anda sendiri, dan ia tidak menghitung pemecahan. Netlify menghitung waktu eksekusi fungsi, jadi fungsi latar belakang yang menunggu pemecahan selama dua menit memakai dua menit dari waktu itu.
Apa bedanya dengan menjalankannya di AWS Lambda?
Batasannya berbeda. Di Lambda di belakang API Gateway, hambatannya adalah timeout integrasi 29 detik, dan NAT gateway dengan Elastic IP memberi setiap pemecahan satu alamat sumber tetap untuk firewall Anda. Di Netlify, hambatannya adalah 60 detik, fungsi latar belakang adalah cara bawaan untuk melewatinya, dan tidak ada alamat tetap kecuali dengan add-on Enterprise. Penyiapan Lambda ada di panduan CAPTCHA AWS Lambda.
Versi singkatnya
Untuk job CAPTCHA di Netlify Functions, jangan pernah memecahkan di fungsi sinkron: batas 60 detiknya tetap, dan waitUntil tidak bisa lolos darinya. Pecahkan di fungsi latar belakang, kirim form di eksekusi yang sama selagi token masih segar, dan klaim job dengan penulisan onlyIfNew lebih dulu agar dua percobaan ulang Netlify tidak pernah bisa mengirim form dua kali. Laporkan hasil dari Blobs melalui fungsi sinkron kecil. Hubungkan ke CapSkip dalam mode Server, dengan alamat dan key di variabel yang ber-scope Functions serta API Key Validation dalam keadaan aktif.
- Setiap tipe CAPTCHA lain yang dipecahkan paket Node: halaman pemecah CAPTCHA Node.js.
Netlify menyediakan fungsinya, dan pemecahan CAPTCHA tetap berlangsung di mesin Windows milik Anda. Itulah gunanya menjalankan sendiri pemecah captcha: meteran platform menghitung menit, dan tidak ada yang menghitung pemecahan.
