Как решить CaptchaFox в Node.js с правильным user agent

Чтобы решить CaptchaFox в Node.js, вызовите метод captchafox из пакета capskip с sitekey и URL страницы, а затем отправьте полученный токен в поле cf-captcha-response. К этой отправке нужно приложить три вещи, которые fetch сам не добавит: cookies, установленные страницей, Referer и user agent, который CapSkip вернул вместе с токеном. Именно на user agent спотыкается большая часть кода на Node. Встроенный fetch в Node сообщает каждому серверу, что его user agent называется node, а сайт, который ждёт токен от выпустившего его браузера, такую отправку отклоняет, каким бы хорошим ни было решение. CapSkip добавил CaptchaFox в версии 1.4.0. В этом руководстве разобраны чтение ключа, вызов, отправка, повторы и одновременный запуск множества регистраций.
Что понадобится
- CapSkip 1.4.0 или новее на машине с Windows. Именно в этом выпуске появился CaptchaFox, а вместе с ним Friendly Captcha и Capy Puzzle.
- Node.js 20.18.1 или новее: это самый старый выпуск, который поддерживает cheerio. В новом проекте используйте 22 или 24, потому что срок поддержки Node 20 закончился в апреле 2026 года.
- Пакет capskip версии 1.3.0 или новее (первый выпуск, в котором есть метод captchafox), а также cheerio для чтения HTML.
- URL страницы, на которой показан виджет. Sitekey, поля формы и сборка виджета берутся именно с этой страницы.
- Адрес решателя. В режиме Local CapSkip отвечает на 127.0.0.1 и только для этого устройства; в режиме Server он слушает ваш сетевой адрес или публичный IP, чтобы скрипт на другой машине мог обращаться к нему через API. Оба режима задаются в разделе Настройки подключения.
# capskip 1.3.0 added captchafox(); cheerio reads the HTML npm install capskip cheerio
Примеры написаны как ES-модули с await верхнего уровня. Сохраняйте их с расширением .mjs или задайте полю type в своём package.json значение module, потому что npm init теперь записывает туда commonjs. Если ваш проект на CommonJS, пакет capskip загружается и через require.
Шаг 1: загружаем страницу, сохраняем её cookies и читаем sitekey
В Node fetch не хранит cookies между вызовами, поэтому сессионный cookie, который ставит страница, к моменту отправки формы уже потерян. Сайт, который привязывает свой CSRF-токен к этой сессии, такую отправку отклонит. Переносите cookies сами с помощью Headers.getSetCookie(), который возвращает каждую строку Set-Cookie отдельно. Руководство по решению Friendly Captcha в Node.js подробнее разбирает это небольшое хранилище cookie.
// npm install capskip cheerio
import * as cheerio from "cheerio";
const PAGE_URL = "https://example.com/signup";
// fetch() keeps no cookies between calls, so carry the page's own.
const jar = new Map();
const page = await fetch(PAGE_URL);
for (const line of page.headers.getSetCookie()) {
const pair = line.split(";")[0];
const eq = pair.indexOf("=");
jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
}
const html = await page.text();
const $ = cheerio.load(html);Sitekey публичен, одинаков для всех посетителей и по соглашению начинается с sk_. Большинство страниц указывают его на контейнере виджета: это div с классом captchafox и атрибутом data-sitekey. Страница, которая строит виджет собственным скриптом, вместо этого передаёт ключ в вызов captchafox.render. Если ни того, ни другого нет в полученном HTML, откройте DevTools: ключом будет сегмент пути после /captcha/ в запросе к api.captchafox.com, а на страницах с MAM в запросе к mam-api.captchafox.com.
Заодно проверьте, из какой сборки загружается виджет. Большинство сайтов загружают его с cdn.captchafox.com и ждут обычный токен. Некоторые платформы вместо этого встраивают пакет с s.uicdn.com/mampkg/, и такие сайты ждут токен, который начинается с MAM_. На странице с MAM ключ может находиться прямо в адресе этого скрипта, после ?key=. Тот же CDN раздаёт и посторонние пакеты, поэтому сверяйте путь к пакету captchafox, а не хост.
// The container, matched on the whole class name.
const widget = $(".captchafox").first();
// MAM pages may carry the key in the package's script src instead.
const mamSrc = $('script[src*="mampkg/@mamdev/core.frontend.libs.captchafox"]').attr("src");
const sitekey = widget.attr("data-sitekey")
?? (mamSrc && new URL(mamSrc, PAGE_URL).searchParams.get("key"))
?? html.match(/captchafox\.render\([\s\S]{0,300}?sitekey\s*:\s*["']([^"']+)/)?.[1];
const form = widget.length ? widget.closest("form") : $("form").first();
console.log(sitekey, Boolean(mamSrc)); // a MAM page needs a MAM_ tokenСелектор класса в cheerio сопоставляет имена классов целиком, поэтому элемент-обёртка с классом вроде captchafox-wrapper случайно не подхватится. Последний запасной вариант читает ключ, переданный прямо в вызов render. Ключа, который лежит в собранном бандле скрипта, в HTML нет вообще, и найти его можно через DevTools. Если контейнера нет, код берёт первую форму на странице. Если на странице есть ещё поле поиска или вторая форма, выбирайте форму регистрации по её id или action.
Шаг 2: вызов captchafox
Чтобы решить CaptchaFox в Node.js, нужен один метод. Он принимает sitekey, URL страницы и необязательный объект опций, а для стандартного виджета ему хватает первых двух.
import { CapSkip } from "capskip";
const solver = new CapSkip({ host: "127.0.0.1", port: 8080 });
// Copied from the script tag on MAM pages.
const MAM_PACKAGE = "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/";
const result = await solver.captchafox(sitekey, PAGE_URL, {
// undefined is dropped, so the standard widget sends nothing extra.
apiServer: mamSrc ? MAM_PACKAGE : undefined,
});
console.log(result.token.slice(0, 24)); // goes in cf-captcha-response
console.log(result.userAgent); // the browser that minted itresult.token и result.code содержат одну и ту же строку, а result.captchaId представляет собой id задания в CapSkip. result.userAgent описывает браузер, который выпустил токен, то есть собственный браузер CapSkip, а не что-либо отправленное вами. Это поле заполняется, только если решение его сообщило, поэтому проверяйте его перед использованием.
URL страницы должен указывать на ту страницу, где виджет действительно работает. CaptchaFox регистрирует каждый ключ на список разрешённых доменов и проверяет хост, прежде чем что-либо выдать. URL страницы на любом другом домене отклоняется при каждой попытке, поэтому сокращённая ссылка, результат поиска или редирект с другого домена не подойдут.
Опции, которые принимает метод
apiServer является псевдонимом api_server, то есть сборки виджета, которую нужно решать. Метод отбрасывает любую опцию со значением undefined или null, поэтому строка выше может передавать её при каждом вызове. Он также принимает объект proxy с ключами type и uri, а ещё useragent, который допускается, но намеренно не применяется, потому что CapSkip решает в настоящем браузере с собственным набором признаков. Параметр timeout для отдельного вызова, в секундах, заменяет recaptchaTimeout клиента для этого решения. Потолок интервала опроса тоже можно задать на один вызов, но только как polling_interval: pollingInterval в camelCase относится к конструктору, и при передаче в отдельный вызов он выбрасывает ValidationException. Пустой sitekey или URL, неизвестная опция и тип прокси, отличный от HTTP, HTTPS, SOCKS5 или SOCKS5H, тоже вызывают ValidationException ещё до того, как запрос покинет вашу машину.
В TypeScript поставляемые с пакетом типы всё ещё описывают token как необязательное поле, предназначенное для ALTCHA, поэтому в режиме strict для result.token нужен non-null assertion. При каждом решении CaptchaFox это значение на месте.
Шаг 3: отправляем под тем user agent, с которым выпущен токен
Когда вы решаете CaptchaFox в Node.js, именно этот шаг определяет, примет ли сайт токен. Из коробки fetch в Node отправляет заголовок User-Agent, в котором стоит одно слово node, причём и на Node 20, и на Node 25. Это не тот браузер, который создал токен, поэтому скопируйте возвращённое значение в запрос, который несёт токен:
// The form's own fields, hidden CSRF token included.
const body = new URLSearchParams(
form.serializeArray().map((f) => [f.name, f.value]),
);
body.set("email", "YOUR_EMAIL");
body.set("cf-captcha-response", result.token);
const headers = {
cookie: [...jar].map(([k, v]) => `${k}=${v}`).join("; "),
// Browsers send the page as Referer; strict servers want one.
referer: PAGE_URL,
};
// Without this line, fetch announces itself as user-agent: node.
if (result.userAgent) headers["user-agent"] = result.userAgent;
const res = await fetch(new URL(form.attr("action") || PAGE_URL, PAGE_URL), {
method: "POST",
headers,
body,
});
console.log(res.status);serializeArray собирает поля, которые отправил бы браузер, включая скрытый CSRF-токен и любые отмеченные чекбоксы, так что называть их вам не придётся. Кнопку отправки он не включает, поэтому если сервер проверяет имя кнопки, добавьте его через body.set. Referer важен не меньше, чем cookie. fetch не отправляет ни Referer, ни Origin, а некоторые фреймворки, в том числе Django, отклоняют отправку формы по HTTPS, в которой нет ни того, ни другого, даже с правильными cookie и токеном.
Ещё два правила следуют из того, как CaptchaFox проверяет токен на стороне сайта. Каждый токен проходит проверку один раз и только в течение короткого времени, поэтому решайте, когда уже готовы, и отправляйте сразу. И считайте токен непрозрачным: никогда не обрезайте и не перекодируйте его. Некоторые сайты отправляют его в теле JSON из собственного скрипта, поэтому один раз отправьте форму вручную с открытыми DevTools и повторите то, что отправляет страница. В документации CaptchaFox по началу работы показаны разметка виджета и проверка на стороне сервера.
Шаг 4: повторы, типы заданий и таймауты
Какое задание выдаст CaptchaFox, выбираете не вы. Чаще всего при решении не выпадает вообще ничего, потому что браузерных признаков хватает самих по себе, а если появляется слайдер-пазл, CapSkip обычно проходит и его. Задания с выбором изображений и аудиозадания редки и не решаются: они возвращаются в виде ApiException, в сообщении которого есть ERROR_CAPTCHA_UNSOLVABLE, причём за считаные секунды, а не по истечении таймаута. При новой попытке обычно выпадает что-то другое, поэтому повтор имеет смысл: либо в CapSkip, либо в вашем коде.
import { ApiException } from "capskip";
async function solveCaptchaFox(sitekey, url, options = {}, attempts = 3) {
for (let attempt = 1; ; attempt++) {
try {
return await solver.captchafox(sitekey, url, options);
} catch (err) {
// Select, audio and unreadable slide draws come back unsolvable;
// redraw. Anything else, such as a wrong API key, is final.
const redraw = err instanceof ApiException && err.message.includes("UNSOLVABLE");
if (!redraw || attempt === attempts) throw err;
}
}
}Сообщение ApiException представляет собой сырой ответ CapSkip, поэтому проверка читает код ошибки прямо из него. Подвох один: ключ, который сайт отклоняет, например на чужом домене, тоже выглядит как нерешаемый и поэтому также уходит в повтор. Он падает за считаные секунды при каждой попытке и ни разу не проходит: по этому признаку его и можно узнать. Другой путь даёт параметр Retries в строке CaptchaFox в настройках CapSkip. Он принимает значения от 0 до 3 и по умолчанию равен 0. Тогда CapSkip сам повторяет попытку при неподдерживаемых заданиях, при попытках, которым не хватило времени, и при ошибках браузера, но никогда не повторяет отклонённый ключ.
captchafox опрашивает результат с таймаутом клиента recaptchaTimeout, по умолчанию 300 секунд, потому что это настоящая браузерная сессия. У CapSkip есть и собственный таймер: задание может ждать одного из 10 потоков CaptchaFox (Max. Threads) до 250 секунд (Wait Timeout), а на одну попытку отводится 150 секунд (Row Timeout). Если поток свободен, неудачная попытка доходит до вас в виде ApiException задолго до того, как истекут 300 секунд SDK. После долгой очереди SDK может первым упереться в свой лимит и выбросить TimeoutException, пока CapSkip ещё работает. Поэтому передавайте более длинный timeout, когда ставите в очередь много решений или поднимаете Retries, ведь каждый повтор может добавить ещё один Row Timeout. Кроме того, SDK продолжает опрос, не останавливаясь на ошибках соединения, поэтому если CapSkip остановится посреди решения, вызов закончится TimeoutException, а не NetworkException.
Полный рабочий пример
Вот весь процесс в одном файле. signUp загружает страницу, читает ключ, решает капчу с повторами и отправляет форму с её cookies, Referer и тем user agent, с которым выпущен токен.
// npm install capskip cheerio
// signup.mjs: run with node signup.mjs
import * as cheerio from "cheerio";
import {
CapSkip, ApiException, CapSkipError, TimeoutException, ValidationException,
} from "capskip";
const PAGE_URL = "https://example.com/signup";
// Copied from the script tag on MAM pages.
const MAM_PACKAGE = "https://s.uicdn.com/mampkg/@mamdev/core.frontend.libs.captchafox/";
const solver = new CapSkip({
apiKey: process.env.CAPSKIP_API_KEY ?? "capskip",
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: Number(process.env.CAPSKIP_PORT ?? 8080),
});
async function solveCaptchaFox(sitekey, url, options = {}, attempts = 3) {
for (let attempt = 1; ; attempt++) {
try {
return await solver.captchafox(sitekey, url, options);
} catch (err) {
// Select, audio and unreadable slide draws come back unsolvable.
const redraw = err instanceof ApiException && err.message.includes("UNSOLVABLE");
if (!redraw || attempt === attempts) throw err;
console.log(`attempt ${attempt}: ${err.message}`);
}
}
}
async function signUp(pageUrl, email) {
// One jar per run, so runs side by side never share a session.
const jar = new Map();
const page = await fetch(pageUrl);
for (const line of page.headers.getSetCookie()) {
const pair = line.split(";")[0];
const eq = pair.indexOf("=");
jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
}
const html = await page.text();
const $ = cheerio.load(html);
const widget = $(".captchafox").first();
const mamSrc = $('script[src*="mampkg/@mamdev/core.frontend.libs.captchafox"]').attr("src");
const sitekey = widget.attr("data-sitekey")
?? (mamSrc && new URL(mamSrc, pageUrl).searchParams.get("key"))
?? html.match(/captchafox\.render\([\s\S]{0,300}?sitekey\s*:\s*["']([^"']+)/)?.[1];
if (!sitekey) throw new Error("No sitekey in the HTML; find it in DevTools.");
const form = widget.length ? widget.closest("form") : $("form").first();
const result = await solveCaptchaFox(sitekey, pageUrl, {
apiServer: mamSrc ? MAM_PACKAGE : undefined,
});
const body = new URLSearchParams(form.serializeArray().map((f) => [f.name, f.value]));
body.set("email", email);
body.set("cf-captcha-response", result.token);
const headers = {
cookie: [...jar].map(([k, v]) => `${k}=${v}`).join("; "),
referer: pageUrl,
};
if (result.userAgent) headers["user-agent"] = result.userAgent;
const action = new URL(form.attr("action") || pageUrl, pageUrl);
return fetch(action, { method: "POST", headers, body });
}
try {
const res = await signUp(PAGE_URL, "YOUR_EMAIL");
console.log(res.status, (await res.text()).slice(0, 80));
} catch (err) {
if (err instanceof ValidationException) console.error(`not sent: ${err.message}`);
else if (err instanceof TimeoutException) console.error("gave up after recaptchaTimeout");
else if (err instanceof CapSkipError) console.error(`solve failed: ${err.name}: ${err.message}`);
else throw err;
process.exitCode = 1;
}Если каждый запуск проваливается за считаные секунды, первым делом проверьте URL страницы, потому что ключ, использованный вне зарегистрированных для него доменов, каждый раз отказывает одинаково. Если проверка sitekey выбрасывает исключение, значит, страница строит виджет из бандла скриптов, и ключ находится на вкладке Network, как описано в шаге 1. Сырой эндпоинт, который стоит за этим методом, описан в разделе CaptchaFox справочника API.
Параллельный запуск множества регистраций
Каждый метод CapSkip в Node возвращает Promise, а AsyncCapSkip представляет собой просто другое имя того же класса, так что параллельный запуск решений сводится к обычному JavaScript. Нужно лишь ограничение. По умолчанию CapSkip выполняет 10 решений CaptchaFox одновременно, а остальные заставляет ждать свободного потока, поэтому с ним согласуется небольшой пул, который держит в работе десять вызовов. Вставьте его вместо блока try в конце полного примера:
// At most `limit` calls in flight; a failure becomes that item's Error.
async function mapLimited(items, limit, fn) {
const results = new Array(items.length);
let next = 0;
async function worker() {
while (next < items.length) {
const i = next++;
results[i] = await fn(items[i]).catch((err) => err);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
return results;
}
const emails = ["YOUR_EMAIL_1", "YOUR_EMAIL_2", "YOUR_EMAIL_3"];
const results = await mapLimited(emails, 10, (email) => signUp(PAGE_URL, email));
for (const r of results) console.log(r instanceof Error ? r.message : r.status);signUp представляет собой функцию из полного примера. При каждом вызове она создаёт собственное хранилище cookie, и именно это делает её безопасной при параллельном запуске: одно хранилище на десять запусков перепутало бы их сессии и CSRF-токены. Кроме того, она держит каждый токен рядом с его собственным user agent, пока отправка не уйдёт. Для пула побольше поднимите Max. Threads в настройках CaptchaFox в CapSkip. CaptchaFox оценивает не только браузер, но и сеть, поэтому множество решений с одного адреса на длинном прогоне повышает частоту заданий. Настройте прокси кнопкой Configure Proxies в той же строке или передавайте объект proxy при каждом вызове.
Запуск решателя на другой машине
Адрес 127.0.0.1 подходит, пока скрипт и CapSkip работают на одном ПК с Windows. Когда код на Node переезжает на VPS, в контейнер, на раннер CI или в serverless-функцию, локальная петля указывает не на ту машину, и первое же решение выбрасывает NetworkException с ECONNREFUSED. Переключите CapSkip в режим Server, и он начнёт слушать ваш сетевой адрес или публичный IP, так что любой из этих вариантов сможет обращаться к нему через тот же API. Если маршрут идёт через интернет, используйте статический публичный IP, включите проверку API-ключа и ограничьте порт ожидаемыми адресами с помощью правила Windows Firewall. Решателем по-прежнему остаётся ваша собственная машина с Windows, и решения по-прежнему не тарифицируются.
Клиент сам не читает переменные окружения. Читайте CAPSKIP_HOST, CAPSKIP_PORT и CAPSKIP_API_KEY в своём коде и передавайте их в конструктор, как это делает полный пример, чтобы один и тот же скрипт работал и у вас на столе, и на сервере.
Частые ошибки и что они означают
| Что вы видите | Причина | Исправить |
|---|---|---|
| Сайт отклоняет токен, который CapSkip вернул как решённый | Отправка ушла с user-agent: node (значение fetch по умолчанию), а не от имени браузера, который выпустил токен | Отправьте result.userAgent в заголовке User-Agent этого запроса |
| Отклоняется, хотя user agent совпадает | Страница загружает пакет MAM, а решение использовало виджет по умолчанию, или наоборот | Прочитайте тег script и задайте apiServer в соответствии с ним |
| Ответ 403 на отправку, хотя токен и user agent верны | Сессионный cookie не был отправлен, поэтому поле CSRF больше не совпадает, или в запросе не было Referer | Перенесите cookies из загрузки страницы и передайте URL страницы как Referer |
| result.userAgent равен undefined | Решение не сообщило user agent, поэтому fetch отправляет свой | Оставьте проверку if; если сайт отклоняет node, отправьте сами user agent актуального десктопного браузера |
| ApiException за считаные секунды, каждый раз, для одного ключа | URL страницы находится вне доменов, для которых зарегистрирован ключ, или неверен сам ключ | Передавайте страницу, на которой работает виджет, а не редирект или результат поиска, и перепроверьте ключ |
| Время от времени ApiException с ERROR_CAPTCHA_UNSOLVABLE | CaptchaFox выдал задание с выбором изображений или аудиозадание либо слайдер, который CapSkip не смог прочитать | Повторите попытку в коде или поднимите Retries в настройках CaptchaFox в CapSkip |
| Токен, который сработал один раз, не проходит во второй | Каждый токен проходит проверку один раз и быстро истекает | Получайте свежий токен для каждой отправки и отправляйте сразу |
| ValidationException ещё до какой-либо отправки | Пустой sitekey или URL, опция, которую метод не принимает, например pollingInterval в отдельном вызове, или тип прокси, который CapSkip не поддерживает | Проверьте, что sitekey найден, и исправьте или уберите опцию, названную в сообщении |
| TimeoutException через пять минут | Ожидание свободного потока CaptchaFox и само решение вместе заняли больше 300 секунд SDK, или CapSkip остановился посреди решения | Ограничьте пул значением Max. Threads или передайте для этого вызова более длинный timeout |
| NetworkException с ECONNREFUSED или ETIMEDOUT на первом вызове | CapSkip не запущен, неверны хост или порт, либо файрвол сбрасывает соединение | Запустите CapSkip и проверьте, в каком режиме он должен работать: Local или Server |
FAQ
Избавляют ли axios или got от проблемы с user agent?
Нет. Они меняют только имя. axios отправляет axios с номером своей версии, а got отправляет got с адресом своего репозитория на GitHub. Ни тот, ни другой не хранит cookies и не отправляет Referer, если вы их так не настроите, поэтому нужны те же три заголовка. Каким бы ни был клиент, задавайте user agent из result.userAgent в том запросе, который несёт токен.
Можно ли вместо этого использовать токен в Puppeteer или Playwright?
Да, если страница, которая его отправляет, работает под тем user agent, который вернул CapSkip. Сначала решите капчу, затем откройте контекст Playwright через browser.newContext({ userAgent: ua }) или в Puppeteer вызовите page.setUserAgent(ua) до загрузки страницы. Загрузите форму, запишите токен в поле cf-captcha-response и отправьте. Если браузер больше ни для чего в вашей автоматизации не нужен, описанный выше путь через fetch легче.
Чем это отличается от версии на Python?
Вызов и правила те же, как и главная ловушка: HTTP-клиент отправляет в качестве user agent собственное имя. В Python этим именем будет python-requests или python-httpx, а requests.Session хранит cookies за вас. В Node же это просто node, и cookies вы переносите сами. Сторона Python разобрана в руководстве по решению CaptchaFox на Python.
Коротко
Чтобы решить CaptchaFox в Node.js, загрузите страницу и сохраните её cookies, считайте sitekey с контейнера captchafox, из вызова render или из скрипта MAM и отметьте, какую сборку загружает страница. Вызовите solver.captchafox() с sitekey и настоящим URL страницы, добавляя apiServer только для страниц с MAM. Отправьте result.token в cf-captcha-response с cookies страницы, самой страницей в Referer и result.userAgent в заголовке User-Agent, один раз и сразу же. Повторяйте попытку, если выпало нерешаемое задание, держите в работе не больше решений, чем Max. Threads (по умолчанию 10), и переходите в режим Server, как только скрипт покидает машину решателя.
- Как работает CaptchaFox и что покрывает решатель: страница решения CaptchaFox.
- Все типы капчи, которые поддерживает пакет capskip, перечислены на странице сервиса распознавания капчи для Node.js.
Единственный повтор, который обычно нужен этому типу, состоит в новой выдаче задания, и когда безлимитный сервис распознавания капчи работает на вашей собственной машине, каждая новая выдача стоит нескольких секунд, а не ещё одного оплачиваемого решения.
