How to Handle CAPTCHA in undetected-chromedriver (Python)

undetected-chromedriver captcha - How to Handle CAPTCHA in undetected-chromedriver (Python)

undetected-chromedriver patches Chrome so the automation flags stop leaking. That gets you onto the page. It does not get you past a reCAPTCHA checkbox, because a widget is not a fingerprint check, it is a token requirement. An undetected-chromedriver captcha still has to be solved and the answer injected into the DOM.

Four steps: start the patched driver, read the sitekey off the page, solve locally, put the token where the page expects it. Here is all of it in Python.

What undetected-chromedriver does and does not do

SignalHandled by the driver
navigator.webdriver and the CDP giveawaysYes
Patched binary so the driver is not detected as oneYes
A reCAPTCHA v2 checkbox on a login formNo
An invisible v2 or v3 challenge fired by the pageNo
A Turnstile widgetNo

The distinction matters because the usual advice for a blocked scraper is “add stealth”. If a widget is on screen, no amount of stealth removes it. You need a token.

What you need

  • Chrome installed, and CapSkip running on 127.0.0.1:8080. The setup guide covers the app.
  • Python 3.10 or newer.
  • Three packages.
# the patched driver, selenium itself, and the local solver
pip install undetected-chromedriver selenium capskip

Step 1: start the patched driver

Import it as uc and build the browser the same way you would with plain Selenium. Options come from uc.ChromeOptions(), not Selenium’s, because the library patches them on the way through:

# pip install undetected-chromedriver
import undetected_chromedriver as uc

options = uc.ChromeOptions()
options.add_argument("--window-size=1280,800")

# use_subprocess keeps the patched binary alive long enough
# to shut down cleanly on Windows.
driver = uc.Chrome(options=options, use_subprocess=True)

driver.get("https://example.com/login")

Do not stack another stealth plugin on top of this. Two patchers fighting over the same properties is a detection signal in itself, and it is a common reason a setup that used to work suddenly stops.

Step 2: read the sitekey off the page

The sitekey is a public attribute on the widget element. Read it from the live DOM rather than hardcoding it, because plenty of sites rotate keys per environment:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# The widget is injected by script, so wait for it.
widget = WebDriverWait(driver, 20).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
)

sitekey = widget.get_attribute("data-sitekey")
page_url = driver.current_url

print(sitekey)

Use driver.current_url rather than the URL you asked for. If the site redirected you to a challenge path, the solver has to be told about the page you actually landed on.

Step 3: solve it locally

CapSkip runs on your own machine, so the solve is a call to localhost. No account, no per-solve billing, nothing leaves the box except the sitekey and the page URL:

# pip install capskip
from capskip import CapSkip

solver = CapSkip(
    host="127.0.0.1",
    port=8080,
    recaptchaTimeout=300,   # seconds, also covers Turnstile
)

result = solver.recaptcha(sitekey=sitekey, url=page_url)

token = result["code"]   # the g-recaptcha-response value

Variants are options rather than different methods. Add invisible=1 for an invisible v2 widget, version="v3" with an action for v3, and enterprise=1 for the Enterprise product.

Step 4: inject the token and fire the callback

The token has to land in the hidden g-recaptcha-response textarea. That alone is enough for a plain form submit, because the field is posted with the rest of the form:

driver.execute_script(
    "document.getElementById('g-recaptcha-response')"
    ".innerHTML = arguments[0];",
    token,
)

driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

Single-page apps are different. They never read the textarea, they wait for the widget’s own callback to hand them the token. The function name is declared on the element, so you can call it directly:

callback = widget.get_attribute("data-callback")

if callback:
    # Call the page's own handler with the token, exactly as
    # the widget would have done.
    driver.execute_script(f"{callback}(arguments[0]);", token)

If nothing happens after injection and there is no data-callback, the page is wiring the callback up in JavaScript instead of markup. The reCAPTCHA v2 callback solver page covers how to find it in that case.

Turnstile in the same driver

Cloudflare Turnstile works the same way with two names changed. The response field is cf-turnstile-response, and it is an input rather than a textarea:

result = solver.turnstile(sitekey=sitekey, url=page_url)

driver.execute_script(
    "document.querySelector('[name=cf-turnstile-response]')"
    ".value = arguments[0];",
    result["code"],
)

That covers a widget embedded in a form. A full-page interstitial challenge needs two extra values and the user agent that the solve was performed with, which the Turnstile solver page explains.

Full working example

# pip install undetected-chromedriver selenium capskip
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from capskip import (
    CapSkip, NetworkException, ApiException, TimeoutException,
)

URL = "https://example.com/login"

solver = CapSkip(host="127.0.0.1", port=8080)
driver = uc.Chrome(options=uc.ChromeOptions(), use_subprocess=True)

try:
    driver.get(URL)

    widget = WebDriverWait(driver, 20).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]"))
    )
    sitekey = widget.get_attribute("data-sitekey")

    result = solver.recaptcha(sitekey=sitekey, url=driver.current_url)

    driver.execute_script(
        "document.getElementById('g-recaptcha-response')"
        ".innerHTML = arguments[0];",
        result["code"],
    )

    callback = widget.get_attribute("data-callback")
    if callback:
        driver.execute_script(f"{callback}(arguments[0]);", result["code"])
    else:
        driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

except NetworkException:
    print("CapSkip is not running on 127.0.0.1:8080")
except (ApiException, TimeoutException) as err:
    print(f"solve failed: {err}")
finally:
    driver.quit()   # always, or the patched binary lingers

Solve and submit in one run. A reCAPTCHA token is accepted for roughly two minutes, so a script that solves, then does thirty seconds of other work, then submits, is fine. One that queues tokens for later is not.

Common errors

What you seeCauseFix
This version of ChromeDriver only supports Chrome version NThe patched driver guessed the wrong Chrome major versionPass it explicitly: uc.Chrome(version_main=N) where N is your installed Chrome major version
OSError: [WinError 6] The handle is invalid on exitThe driver was garbage collected before it shut downCall driver.quit() in a finally block and keep use_subprocess=True
NoSuchElementException when locating [data-sitekey]The widget is in an iframe, or has not rendered yetWait for it as above. For an iframe, read the sitekey from the iframe src query string instead
ERROR_GOOGLEKEYAn empty or truncated sitekey reached the solverPrint it before solving. See fixing ERROR_GOOGLEKEY
NetworkExceptionCapSkip is not running, or the port differsStart the app and check the port in its settings
Token injected, form still refusedThe page uses a callback and never reads the textareaCall data-callback with the token

Every error code and parameter is listed in the API documentation.

Frequently asked questions

Does undetected-chromedriver solve CAPTCHAs by itself?

No. It hides the automation signals that get a browser flagged. A rendered widget still expects a valid token, so you solve it and inject the result. The two jobs are complementary, not alternatives.

Can I run it headless?

You can, with uc.Chrome(headless=True), which applies the library’s own headless patches rather than Selenium’s flag. Expect more challenges than in headed mode. Token injection itself works identically either way.

Do I need a proxy for the solve?

Only if the browser is already going through one. Pass the same one as proxy={"type": "HTTPS", "uri": "user:pass@host:port"} so the solve and the submit share a network path. Proxies apply to reCAPTCHA, Turnstile and GeeTest, never to image CAPTCHAs.

Does this work with plain Selenium too?

Yes. Steps 2 to 4 are ordinary WebDriver calls, so they run unchanged on standard Selenium, on Selenium Grid, and on remote drivers. Only step 1 is specific to the patched library.

Summary

Let undetected-chromedriver handle the fingerprint, and handle the widget yourself. Wait for [data-sitekey], solve against localhost, write the token into g-recaptcha-response, and call data-callback when the page declares one. Quit the driver in a finally block so the patched binary does not linger.

The wider Selenium surface is on the Selenium CAPTCHA solver page, the full method list is on the Python CAPTCHA solver page, and CAPTCHA solving for web scraping covers running this at volume. CapSkip is a local captcha solver, so every solve above happens on your own machine.