Tutorials

Menangani Beberapa CAPTCHA dalam Satu Halaman

Script Anda mengirim token, mendapat respons sukses dari CaptchaAI, tapi form tetap menampilkan "verifikasi gagal." Setelah ditelusuri, ternyata halaman itu merender dua widget reCAPTCHA sekaligus — satu untuk form login, satu lagi untuk pendaftaran newsletter — dan integrasi Anda hanya mendeteksi salah satunya.

Kasusnya lebih umum dari yang terlihat: checkout multi-step, halaman onboarding dengan beberapa section, atau kombinasi reCAPTCHA dan Cloudflare Turnstile pada widget berbeda di form yang sama. Setiap CAPTCHA punya sitekey dan target element sendiri, jadi solusinya bukan menambah retry — melainkan mendeteksi semua widget, solve secara paralel, dan inject setiap token ke callback yang tepat.


Kenapa satu halaman bisa merender lebih dari satu CAPTCHA

Tim QA yang menguji alur checkout atau registrasi lengkap akan menemukan pola ini terus berulang, bukan sebagai kasus tepi yang jarang terjadi. Penyebabnya biasanya salah satu dari tiga hal:

  • Form multi-step yang memvalidasi ulang di tiap langkah — satu CAPTCHA per step.
  • Widget pihak ketiga (newsletter, live chat, form kontak) yang masing-masing punya proteksi sendiri di luar form utama.
  • Kombinasi proteksi Google (reCAPTCHA) dengan proteksi level Cloudflare (Turnstile) pada halaman yang sama.

Konteks lokal: multi-CAPTCHA di form checkout dan onboarding Indonesia

Tim QA freelance yang menangani proyek client lewat Upwork atau Fastwork sering menemui pola ini saat menguji form checkout multi-step milik toko online atau platform travel — satu step verifikasi login, step berikutnya verifikasi pembayaran, masing-masing dengan widget CAPTCHA sendiri. Kalau worker testing Anda di-deploy ke region seperti AWS ap-southeast-1 (Singapura) atau GCP asia-southeast2 (Jakarta), latensi ke situs staging biasanya lebih rendah dibanding region default us-east-1 — relevan kalau timeout polling Anda ketat, karena dua atau tiga CAPTCHA paralel butuh slot thread yang cukup di waktu bersamaan.

Dari sisi kapasitas, solve tiga CAPTCHA paralel berarti Anda butuh minimal tiga thread yang tersedia di saat yang sama. Paket STANDARD ($30/bulan, 15 thread) biasanya cukup lega untuk beberapa worker testing berjalan bersamaan, sementara ADVANCE ($90/bulan, 50 thread) lebih cocok kalau Anda menjalankan banyak instance browser sekaligus. Karena tiap thread punya solve tanpa batas, biayanya tetap flat berapa pun jumlah CAPTCHA yang Anda proses dalam sebulan.

Kalau form yang diuji menampilkan data pengguna asli, pastikan pengujian tetap berjalan di staging, bukan data pribadi nyata — sejalan dengan UU Pelindungan Data Pribadi (UU 27/2022). Ini bukan nasihat hukum, sekadar pengingat praktik yang aman untuk tim QA.


Langkah 1: deteksi semua CAPTCHA di halaman sekaligus

Sebelum solve apa pun, kumpulkan dulu sitekey dan referensi element dari setiap widget yang ada di halaman. Kalau integrasi Anda hanya menangkap CAPTCHA pertama yang ditemukan, hasilnya sama saja dengan tidak menangani apa pun — token tetap kosong di widget kedua.

Python + Selenium: ambil sitekey dari tiap iframe reCAPTCHA

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com/multi-captcha-page")

# Find all reCAPTCHA iframes
captcha_iframes = driver.find_elements(
    By.CSS_SELECTOR, 'iframe[src*="recaptcha/api2/anchor"]'
)

# Extract sitekeys from each iframe's src
import re
captchas = []
for i, iframe in enumerate(captcha_iframes):
    src = iframe.get_attribute("src")
    match = re.search(r"k=([A-Za-z0-9_-]+)", src)
    if match:
        captchas.append({
            "index": i,
            "sitekey": match.group(1),
            "iframe": iframe
        })

print(f"Found {len(captchas)} CAPTCHAs on page")
for c in captchas:
    print(f"  [{c['index']}] sitekey: {c['sitekey']}")

Output-nya menunjukkan index dan sitekey tiap widget — informasi ini jadi acuan waktu Anda submit dan inject token nanti.

Found 2 CAPTCHAs on page
  [0] sitekey: 6LcXyzABCDEF-login
  [1] sitekey: 6LcAbcDEFGHI-signup

JavaScript + Puppeteer: scan semua widget .g-recaptcha di DOM

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  await page.goto('https://example.com/multi-captcha-page', {
    waitUntil: 'networkidle2'
  });

  // Extract all reCAPTCHA widgets from the page
  const captchas = await page.evaluate(() => {
    const widgets = document.querySelectorAll('.g-recaptcha');
    return Array.from(widgets).map((el, i) => ({
      index: i,
      sitekey: el.getAttribute('data-sitekey'),
      elementId: el.id || `captcha-${i}`,
      callbackName: el.getAttribute('data-callback') || null
    }));
  });

  console.log(`Found ${captchas.length} CAPTCHAs`);
  captchas.forEach(c => console.log(`  [${c.index}] ${c.sitekey}`));
})();

Puppeteer membaca atribut data-callback langsung dari DOM, jadi Anda sudah tahu di awal nama fungsi callback yang harus dipanggil untuk tiap widget — berguna di langkah inject nanti.


Langkah 2: solve semua CAPTCHA secara paralel, bukan satu per satu

Solve satu per satu berarti total waktu tunggu adalah jumlah dari semua solve time — dua CAPTCHA yang masing-masing butuh 15–30 detik bisa berarti hampir satu menit total. Kirim semuanya ke CaptchaAI sekaligus, lalu polling secara paralel, dan total waktu tunggu turun jadi setara solve satu CAPTCHA saja.

Python: submit dan polling dengan ThreadPoolExecutor

import requests
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

API_KEY = "YOUR_API_KEY"
PAGE_URL = "https://example.com/multi-captcha-page"

def submit_captcha(sitekey):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": PAGE_URL,
        "json": "1"
    })
    result = resp.json()
    if result["status"] != 1:
        raise Exception(f"Submit error: {result['request']}")
    return result["request"]

def poll_result(task_id, timeout=120):
    deadline = time.time() + timeout
    while time.time() < deadline:
        time.sleep(5)
        resp = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY,
            "action": "get",
            "id": task_id,
            "json": "1"
        })
        result = resp.json()
        if result["status"] == 1:
            return result["request"]
        if result["request"] != "CAPCHA_NOT_READY":
            raise Exception(f"Poll error: {result['request']}")
    raise TimeoutError(f"Task {task_id} timed out")

def solve_all(captchas):
    # Submit all in parallel
    task_ids = {}
    with ThreadPoolExecutor(max_workers=len(captchas)) as pool:
        futures = {
            pool.submit(submit_captcha, c["sitekey"]): c["index"]
            for c in captchas
        }
        for future in as_completed(futures):
            idx = futures[future]
            task_ids[idx] = future.result()
            print(f"[captcha-{idx}] Submitted → task {task_ids[idx]}")

    # Poll all in parallel
    tokens = {}
    with ThreadPoolExecutor(max_workers=len(task_ids)) as pool:
        futures = {
            pool.submit(poll_result, tid): idx
            for idx, tid in task_ids.items()
        }
        for future in as_completed(futures):
            idx = futures[future]
            tokens[idx] = future.result()
            print(f"[captcha-{idx}] Solved")

    return tokens

# Example usage
captchas = [
    {"index": 0, "sitekey": "6LcXyzABCDEF-login"},
    {"index": 1, "sitekey": "6LcAbcDEFGHI-signup"}
]

tokens = solve_all(captchas)

Hasil polling paralel terlihat seperti ini — urutan penyelesaian tidak harus sama dengan urutan submit, dan itu wajar.

[captcha-0] Submitted → task 71823456
[captcha-1] Submitted → task 71823457
[captcha-1] Solved
[captcha-0] Solved

Langkah 3: inject token ke widget yang tepat, bukan sekadar yang pertama ditemukan

Setiap widget reCAPTCHA punya textarea g-recaptcha-response sendiri, bersarang di dalam container widget-nya masing-masing. Inject ke textarea yang salah — atau lupa memicu callback-nya — dan form tetap menganggap CAPTCHA itu belum diselesaikan meskipun token-nya valid.

Python (Selenium)

def inject_tokens(driver, captchas, tokens):
    for c in captchas:
        idx = c["index"]
        token = tokens[idx]

        # Find the textarea within the widget's container
        container = driver.find_elements(By.CSS_SELECTOR, ".g-recaptcha")[idx]
        textarea = container.find_element(
            By.CSS_SELECTOR, 'textarea[name="g-recaptcha-response"]'
        )
        driver.execute_script(
            "arguments[0].value = arguments[1];", textarea, token
        )

        # Trigger the callback if defined
        callback = c.get("callback")
        if callback:
            driver.execute_script(f"{callback}('{token}');")

        print(f"[captcha-{idx}] Token injected")

inject_tokens(driver, captchas, tokens)

JavaScript (Puppeteer)

async function injectTokens(page, captchas, tokens) {
  for (const captcha of captchas) {
    const token = tokens[captcha.index];

    await page.evaluate((idx, tkn, callbackName) => {
      const widgets = document.querySelectorAll('.g-recaptcha');
      const textarea = widgets[idx].querySelector(
        'textarea[name="g-recaptcha-response"]'
      );
      textarea.value = tkn;

      if (callbackName && typeof window[callbackName] === 'function') {
        window[callbackName](tkn);
      }
    }, captcha.index, token, captcha.callbackName);

    console.log(`[captcha-${captcha.index}] Token injected`);
  }
}

await injectTokens(page, captchas, tokens);

Kalau tipe CAPTCHA-nya campuran: reCAPTCHA plus Turnstile dalam satu form

Tidak semua halaman multi-CAPTCHA memakai tipe yang sama. Kombinasi paling umum yang ditemui di lapangan:

  • reCAPTCHA v2 untuk form login atau pendaftaran.
  • Cloudflare Turnstile untuk proteksi di level halaman atau checkout.
  • Keduanya sekaligus, pada halaman yang menggabungkan proteksi Google dengan proteksi Cloudflare.

Deteksi tiap tipe secara terpisah, lalu kirim dengan parameter method yang sesuai:

def detect_all_captchas(driver):
    detected = []

    # reCAPTCHA
    recaptchas = driver.find_elements(By.CSS_SELECTOR, ".g-recaptcha")
    for i, el in enumerate(recaptchas):
        detected.append({
            "type": "userrecaptcha",
            "sitekey": el.get_attribute("data-sitekey"),
            "label": f"recaptcha-{i}"
        })

    # Turnstile
    turnstiles = driver.find_elements(By.CSS_SELECTOR, ".cf-turnstile")
    for i, el in enumerate(turnstiles):
        detected.append({
            "type": "turnstile",
            "sitekey": el.get_attribute("data-sitekey"),
            "label": f"turnstile-{i}"
        })

    return detected

Kirim masing-masing dengan method yang cocok — userrecaptcha untuk reCAPTCHA, turnstile untuk Cloudflare Turnstile. Selebihnya, alur submit-polling-inject di atas berlaku sama persis untuk kedua tipe.


Kesalahan yang paling sering terjadi dan cara memperbaikinya

Masalah deteksi

Gejala Penyebab Cara memperbaiki
Hanya CAPTCHA pertama yang terdeteksi CAPTCHA kedua baru dirender belakangan (lazy load) Tunggu semua iframe/widget muncul dulu sebelum mulai scan
ERROR_WRONG_GOOGLEKEY Sitekey salah diekstrak Verifikasi ulang dari atribut src iframe atau data-sitekey

Masalah token dan inject

Gejala Penyebab Cara memperbaiki
Token sudah di-inject tapi form tetap menolak Callback tidak ikut terpicu Periksa atribut data-callback dan panggil fungsinya manual dengan token
Token masuk ke widget yang salah Index tidak sinkron dengan urutan render Petakan token ke sitekey masing-masing widget, jangan andalkan posisi index

Pertanyaan umum

Apakah setiap CAPTCHA di halaman yang sama harus punya sitekey berbeda?

Tidak harus berbeda, tapi jangan diasumsikan sama. Setiap widget bisa memakai sitekey sendiri-sendiri, jadi selalu ekstrak sitekey per widget alih-alih menyamakannya dengan CAPTCHA pertama yang Anda temukan.

Solve tiga atau empat CAPTCHA sekaligus butuh paket thread yang lebih besar?

Tergantung berapa banyak yang berjalan bersamaan. Kalau worker Anda memproses beberapa halaman multi-CAPTCHA secara paralel, jumlah thread yang aktif di waktu yang sama biasanya lebih menentukan daripada volume CAPTCHA per hari — cocokkan dengan kebutuhan concurrency Anda sebelum upgrade dari BASIC ke paket yang lebih besar.

Salah satu dari beberapa CAPTCHA gagal di-solve, apakah proses lainnya ikut batal?

Tidak, kalau ditangani dengan benar. ThreadPoolExecutor pada contoh di atas menjalankan tiap solve sebagai task independen — satu task yang gagal atau timeout tidak menghentikan future lain yang sedang polling, asal Anda menangkap exception per task, bukan membungkus semuanya dalam satu blok try besar.

CAPTCHA kedua baru muncul setelah saya submit form pertama, apa yang harus diubah?

Scan ulang halaman setelah setiap submit, bukan hanya sekali di awal. Pakai WebDriverWait di Selenium atau page.waitForSelector di Puppeteer untuk menunggu widget baru benar-benar dirender sebelum mengulang proses deteksi-solve-inject.

Teknik ini berlaku juga untuk CAPTCHA yang dirender lewat iframe pihak ketiga, bukan widget Google langsung?

Prinsipnya sama — deteksi tiap sumber secara terpisah, solve paralel, inject ke container masing-masing — tapi cara mengekstrak sitekey dari iframe bersarang berbeda. Kalau widget-nya ada di dalam iframe nested, lihat ekstraksi CAPTCHA dari iframe bersarang untuk pola ekstraksinya.


Panduan Terkait

Komentar dinonaktifkan untuk artikel ini.