{"id":25482,"date":"2026-09-03T07:40:47","date_gmt":"2026-09-03T07:40:47","guid":{"rendered":"https:\/\/capskip.com\/?p=25482"},"modified":"2026-09-03T07:40:47","modified_gmt":"2026-09-03T07:40:47","slug":"apify-actor-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/apify-actor-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Apify Actor \u91cc\u8bc6\u522b\u9a8c\u8bc1\u7801\uff08Node.js\uff09"},"content":{"rendered":"<p>An Apify captcha step is the usual three moves, plus one that is specific to the platform. Read the sitekey, solve it, post the token back with the form. The extra move is deciding where the solver lives, because an Actor does not run on your laptop. It runs in a container on Apify&#8217;s infrastructure, so the loopback address inside it belongs to that container and nothing else. Get that one decision right and the rest is twenty lines.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Node.js 18 or newer, the Apify CLI, and an Apify account.<\/li>\n<li>The apify and capskip packages installed in the Actor.<\/li>\n<li>CapSkip running in Server mode on a machine the Actor can reach, with a static public IP and key validation switched on. Local mode still works while you develop on your own machine. Both modes are described under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Scaffold an Actor, then add the solver client.\napify create captcha-actor -t getting_started_node\ncd captcha-actor\nnpm install capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Server mode is not optional here<\/h2>\n<p>This is the part that trips people up, so it goes first. Actors execute on Apify&#8217;s workers. When your Actor code opens a connection to 127.0.0.1:8080 it is talking to its own container, which is not running a solver, and the call fails with a connection error that looks like the solver crashed. Nothing crashed. The address was simply local to the wrong machine.<\/p>\n<p>CapSkip has two connection modes for exactly this reason. Local binds to 127.0.0.1 and answers only that device. Server binds to your network or public IP, so another box, a VPS or a hosted platform such as Apify can call the same solver over the API. A static public IP keeps the address stable between runs.<\/p>\n<p>Worth saying plainly, because it comes up: Server mode does not turn CapSkip into a metered cloud service. It is still your machine and still unlimited. All that changes is which interface it listens on. Once it is listening on a network address, switch key validation on and give the Actor its own key, so that key can be revoked without disturbing anything else.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: declare the solver address as a secret input<\/h2>\n<p>Do not hardcode the host. Apify&#8217;s input schema supports encrypted fields, which is the right home for a solver address and its key, and it means the values are set per run rather than baked into a build. Encryption works with the textfield, textarea and hidden editors.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"json\" class=\"EnlighterJSRAW\">{\n  &quot;title&quot;: &quot;CAPTCHA actor input&quot;,\n  &quot;type&quot;: &quot;object&quot;,\n  &quot;schemaVersion&quot;: 1,\n  &quot;properties&quot;: {\n    &quot;targetUrl&quot;: {\n      &quot;title&quot;: &quot;Target URL&quot;,\n      &quot;type&quot;: &quot;string&quot;,\n      &quot;editor&quot;: &quot;textfield&quot;\n    },\n    &quot;solverHost&quot;: {\n      &quot;title&quot;: &quot;Solver host&quot;,\n      &quot;type&quot;: &quot;string&quot;,\n      &quot;editor&quot;: &quot;textfield&quot;,\n      &quot;isSecret&quot;: true\n    },\n    &quot;solverKey&quot;: {\n      &quot;title&quot;: &quot;Solver API key&quot;,\n      &quot;type&quot;: &quot;string&quot;,\n      &quot;editor&quot;: &quot;textfield&quot;,\n      &quot;isSecret&quot;: true\n    }\n  },\n  &quot;required&quot;: [&quot;targetUrl&quot;, &quot;solverHost&quot;]\n}<\/pre>\n<\/div>\n<p>That file goes in the .actor folder next to actor.json, and the values arrive in your code through the input object.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: pick the host at runtime<\/h2>\n<p>You want one Actor that works in both places: talking to 127.0.0.1 while you run it locally, and talking to your server once it is deployed. The SDK exposes a boolean for precisely this. Actor.isAtHome() returns true when the code is executing on the Apify platform and false when it is not.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install apify capskip\nimport { Actor } from 'apify';\nimport { CapSkip } from 'capskip';\n\nawait Actor.init();\nconst input = await Actor.getInput();\n\n\/\/ Local run talks to the loopback address. A platform run\n\/\/ talks to the server address that came in as a secret.\nconst solver = new CapSkip({\n  host: Actor.isAtHome() ? input.solverHost : '127.0.0.1',\n  port: 8080,\n  apiKey: input.solverKey,\n});<\/pre>\n<\/div>\n<p>Nothing else in the Actor has to know about the difference. The same code path handles both, and a failed deploy no longer means editing constants.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: read the sitekey, solve it, post the token<\/h2>\n<p>The sitekey sits on the host document as a data-sitekey attribute, so a plain fetch and a regular expression get it without starting a browser. That matters on a paid platform: an Actor with no browser needs far less memory, and Apify bills on memory multiplied by time. Actor.fail() below ends the run rather than returning, which is why the code after it can assume the match succeeded.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ Fetch the form page and lift the sitekey out of it.\nconst html = await (await fetch(input.targetUrl)).text();\nconst match = html.match(\/data-sitekey=&quot;([^&quot;]+)&quot;\/);\n\nif (!match) {\n  await Actor.fail('No sitekey on the page. Did the widget render?');\n}\n\nconst result = await solver.recaptcha(match[1], input.targetUrl);\nconst token = result.code;   \/\/ the g-recaptcha-response value<\/pre>\n<\/div>\n<p>Then submit the form with the token in the field the site expects. For reCAPTCHA v2 the hidden textarea is named g-recaptcha-response, and most forms post it under that same name. If the page hands the token to a callback instead, check what the callback actually submits.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ The token travels as an ordinary form field.\nconst body = new URLSearchParams({\n  email: 'someone@example.com',\n  'g-recaptcha-response': token,\n});\n\nconst posted = await fetch(input.targetUrl, { method: 'POST', body });\nawait Actor.pushData({ url: input.targetUrl, status: posted.status });<\/pre>\n<\/div>\n<p>Turnstile and GeeTest have their own methods on the same client, and both take the same shape. Turnstile also returns a user agent that has to be sent with the token on challenge pages. Full parameter lists are in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>The whole Actor, as src\/main.js. An Actor is an ES module with top-level await, so there is no wrapper function, and Actor.exit() at the end is what flushes the dataset and closes the run cleanly.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install apify capskip\nimport { Actor } from 'apify';\nimport { CapSkip, NetworkException, TimeoutException } from 'capskip';\n\nawait Actor.init();\n\nconst input = await Actor.getInput();\nconst solver = new CapSkip({\n  host: Actor.isAtHome() ? input.solverHost : '127.0.0.1',\n  port: 8080,\n  apiKey: input.solverKey,\n});\n\ntry {\n  const html = await (await fetch(input.targetUrl)).text();\n  const match = html.match(\/data-sitekey=&quot;([^&quot;]+)&quot;\/);\n  if (!match) throw new Error('No sitekey found on the page.');\n\n  const result = await solver.recaptcha(match[1], input.targetUrl);\n\n  const body = new URLSearchParams({\n    'g-recaptcha-response': result.code,\n  });\n  const posted = await fetch(input.targetUrl, { method: 'POST', body });\n\n  await Actor.pushData({ url: input.targetUrl, status: posted.status });\n} catch (err) {\n  if (err instanceof NetworkException) {\n    await Actor.fail('Cannot reach the solver. Check Server mode and the host.');\n  }\n  if (err instanceof TimeoutException) {\n    await Actor.fail('The solve outlasted recaptchaTimeout.');\n  }\n  throw err;\n}\n\nawait Actor.exit();<\/pre>\n<\/div>\n<p>Catching the two connection-shaped exceptions separately is worth the six lines. Actor.fail() is Actor.exit() with an exit code of 1 and a message attached, so the run ends as FAILED with a sentence in the log that tells you which half of the setup broke. Without it, a solver that is simply unreachable and a solve that genuinely could not be read produce the same red run.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running it locally before you deploy<\/h2>\n<p>Run the Actor on your own machine first, with the solver in Local mode. isAtHome() returns false there, so the code reaches for 127.0.0.1 without you changing anything, and you get to prove the sitekey read and the form post work before adding the network hop.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Reads INPUT from storage\/key_value_stores\/default.\napify run<\/pre>\n<\/div>\n<p>When that passes, switch CapSkip to Server mode, note the address it now listens on, and put that address in the solverHost input on the platform. The only thing that changed is one string.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>NetworkException on the platform, never locally<\/td>\n<td>The Actor called 127.0.0.1 and reached its own container<\/td>\n<td>Switch the solver to Server mode and pass its address<\/td>\n<\/tr>\n<tr>\n<td>ERROR_WRONG_USER_KEY<\/td>\n<td>Key validation is on and the Actor sent the wrong key<\/td>\n<td>Set the key as a secret input and read it from the input<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY<\/td>\n<td>The regular expression matched nothing and an empty key went out<\/td>\n<td>Check the match before spending a solve on it<\/td>\n<\/tr>\n<tr>\n<td>Run status TIMED-OUT<\/td>\n<td>The run&#8217;s timeout is shorter than fetch plus solve plus post<\/td>\n<td>Raise the timeout in the Actor&#8217;s default run options<\/td>\n<\/tr>\n<tr>\n<td>TimeoutException<\/td>\n<td>The solve outlasted recaptchaTimeout<\/td>\n<td>Raise it above the default 300 seconds<\/td>\n<\/tr>\n<tr>\n<td>The form rejects a token that solved fine<\/td>\n<td>The token aged out between the solve and the post<\/td>\n<td>Solve immediately before posting, not at the start of the run<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can an Apify Actor really reach a solver on my own machine?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, over the same API it would use for any internal service. Server mode makes CapSkip listen on your network or public IP instead of the loopback address, so the Actor calls it like any other HTTP endpoint. A static public IP is recommended so the address does not move between runs, and key validation should be on before you expose the port.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does this work inside a Crawlee crawler on Apify?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">It does, and the three moves are identical. The difference is where they sit: the sitekey read and the token injection go inside the request handler, and the solver client is created once outside it so every request shares one instance. The crawler-specific detail, including why you should not wrap a solve in your own retry loop, is covered in the <a href=\"https:\/\/capskip.com\/crawlee-captcha\/\">Crawlee CAPTCHA guide<\/a>.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Should I solve through Apify Proxy?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Only if the site scores the IP that solved. Proxy support exists for reCAPTCHA, Turnstile and GeeTest, and it matters when the token is checked against the address that requested it. Pass the proxy on the solve call rather than routing the whole solver through it, so the fetch and the solve can use different exits when that is what you want. The tradeoffs are set out in the guide to <a href=\"https:\/\/capskip.com\/captcha-proxy-rotation\/\">CAPTCHA proxy rotation<\/a>.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">How much memory should the Actor get?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Less than you think, if you skip the browser. The Actor above fetches HTML, waits on a network call and posts a form, so it is idle for most of its life and needs nothing like a Chromium footprint. Reach for a browser only when the page will not give up its sitekey without running JavaScript. The solving itself happens on the solver&#8217;s machine either way, though your Actor is still running while it waits.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Put the solver in Server mode, store its address and key as encrypted inputs, and let Actor.isAtHome() choose between that address and 127.0.0.1. Then read the sitekey, solve it, and post the token as g-recaptcha-response. For the wider Node picture, including Puppeteer and Playwright, see <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js CAPTCHA solver page<\/a>. The crawling side of the same problem is covered on <a href=\"https:\/\/capskip.com\/captcha-solver-for-web-scraping\/\">the web scraping page<\/a>.<\/p>\n<p>One consequence is worth spelling out before you scale an Actor up. Because this <a href=\"https:\/\/capskip.com\/\">captcha bypass<\/a> runs on hardware you already own, going from ten runs to a thousand moves your Apify bill and leaves your solving bill where it was.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Actor \u8fd0\u884c\u5728 Apify \u7684\u4e91\u4e0a\uff0c\u6240\u4ee5 127.0.0.1 \u6307\u7684\u662f\u5bb9\u5668\uff0c\u800c\u4e0d\u662f\u4f60\u7684\u7535\u8111\u3002\u8ba9\u8bc6\u522b\u5de5\u5177\u4ee5 Server \u6a21\u5f0f\u8fd0\u884c\uff0c\u628a\u5b83\u7684\u5730\u5740\u5b58\u6210 secret \u7c7b\u578b\u7684\u8f93\u5165\uff0c\u518d\u7528 isAtHome() \u5207\u6362 host\u3002<\/p>","protected":false},"author":1,"featured_media":25481,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Apify CAPTCHA: Solve It Inside an Actor | CapSkip","rank_math_description":"An apify captcha solve never reaches 127.0.0.1: an Actor runs in Apify's own container. Put the solver in Server mode and switch hosts with isAtHome().","rank_math_focus_keyword":"apify captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25482","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25482","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=25482"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25482\/revisions"}],"predecessor-version":[{"id":25486,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25482\/revisions\/25486"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25481"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25482"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25482"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25482"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}