{"id":25305,"date":"2026-08-24T04:41:41","date_gmt":"2026-08-24T04:41:41","guid":{"rendered":"https:\/\/capskip.com\/?p=25305"},"modified":"2026-08-24T04:41:41","modified_gmt":"2026-08-24T04:41:41","slug":"cypress-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/cypress-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Cypress \u6d4b\u8bd5\u4e2d\u7528 cy.task \u8bc6\u522b\u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>A Cypress captcha problem is a runtime problem before it is a solving problem. Your spec code runs inside the browser under test, so the Node client that talks to the solver cannot live there. Register it as a task in cypress.config.js instead, call that task from the spec, and write the token into the hidden field yourself. Three things make it work: the task, a raised timeout, and a direct DOM write rather than a Cypress click. This guide covers all three.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Cypress 10 or newer, which is where cypress.config.js and setupNodeEvents arrived. Anything older uses the old plugins file and the same idea still applies<\/li>\n<li>Node.js 18 or newer<\/li>\n<li>CapSkip running and reachable. Local mode listens on 127.0.0.1 port 8080 for a test run on the same machine, and Server mode listens on your network or public IP so a CI runner or another box can call it. Both are in the <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a><\/li>\n<li>The solver client, installed as a dev dependency<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># The client only ever runs in the Node half of Cypress.\nnpm install --save-dev capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why the solve cannot go in the spec<\/h2>\n<p>Cypress splits into two processes and this is the whole reason the naive version fails. Your spec file is bundled and executed inside the browser, next to the application. Everything in cypress.config.js runs in Node, outside it.<\/p>\n<p>Requiring the solver client at the top of a spec therefore pulls a Node HTTP client into a browser bundle. Even when the bundler lets that through, the browser then blocks the call: a request from your app&#8217;s origin to 127.0.0.1 on port 8080 is cross origin, and the solver is not sending CORS headers to make it legal.<\/p>\n<p>Cypress gives you two doors into Node, and both are fine:<\/p>\n<ul>\n<li><strong>cy.task<\/strong> runs an arbitrary function you registered in the config. This is where the SDK belongs, because its polling and backoff logic then runs in Node where it was designed to.<\/li>\n<li><strong>cy.request<\/strong> makes the HTTP call from the Cypress Node process rather than the browser, which is why the Cypress documentation says it bypasses CORS entirely. Good if you would rather hit the raw API and skip the dependency.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: register the solve as a task<\/h2>\n<p>One function, registered once, available to every spec.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install --save-dev capskip\nconst { defineConfig } = require('cypress');\nconst { CapSkip } = require('capskip');\n\n\/\/ Local mode. Point host at a server IP to share one solver.\nconst solver = new CapSkip({ host: '127.0.0.1', port: 8080 });\n\nmodule.exports = defineConfig({\n  \/\/ 60000 is the default, and a v2 solve can outlast it.\n  taskTimeout: 180000,\n\n  e2e: {\n    setupNodeEvents(on) {\n      on('task', {\n        async solveRecaptcha({ sitekey, url }) {\n          const result = await solver.recaptcha(sitekey, url);\n          return result.code;   \/\/ the token\n        },\n      });\n    },\n  },\n});<\/pre>\n<\/div>\n<p>One rule about tasks that costs everyone an hour the first time: a task must return a value or null, never undefined. Forget the return statement and Cypress fails the command with a message about the task returning undefined, which reads like the solver broke when the solver was never called.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: call the task and inject the token<\/h2>\n<p>Read the sitekey off the page, hand it to the task, and put the answer where the widget would have put it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ cypress\/e2e\/login.cy.js\nit('logs in through the reCAPTCHA', () =&gt; {\n  cy.visit('\/login');\n\n  cy.get('[data-sitekey]')\n    .invoke('attr', 'data-sitekey')\n    .then((sitekey) =&gt; {\n      const url = 'https:\/\/example.com\/login';\n\n      cy.task('solveRecaptcha', { sitekey, url }).then((token) =&gt; {\n        \/\/ The widget writes into a hidden textarea. Do the same.\n        cy.document().then((doc) =&gt; {\n          doc.getElementById('g-recaptcha-response').value = token;\n        });\n      });\n    });\n\n  cy.get('button[type=submit]').click();\n  cy.contains('Welcome back');\n});<\/pre>\n<\/div>\n<p>Note the DOM write. The response field is a hidden textarea, and Cypress refuses to type into an element it considers invisible, so the obvious version with a get and a type call fails on visibility before it ever gets near the token. Going through cy.document sidesteps that, exactly as the widget&#8217;s own JavaScript would.<\/p>\n<p>If the widget div carries a data-callback attribute, invoke that function with the token instead of clicking the submit button. Pages built that way never wire up a normal form submit, so the click does nothing.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The timeout that actually bites is taskTimeout<\/h2>\n<p>This is the failure that gets blamed on the solver most often, and the fix is one line in the config.<\/p>\n<p>Cypress gives a task <strong>60 seconds<\/strong> by default. A reCAPTCHA v2 job is not ready for the first 15 to 20 seconds, v3 takes 10 to 15, and a busy machine can stretch either. When the ceiling hits, Cypress kills the command and the test fails with a timeout that names your task, not the CAPTCHA.<\/p>\n<p>The trap next to it is raising the wrong number. Most Cypress timeout advice points at defaultCommandTimeout, which is 4000 milliseconds and governs DOM commands. It has no effect on a task. Three values matter here and they are all separate:<\/p>\n<table>\n<thead>\n<tr>\n<th>Option<\/th>\n<th>Default<\/th>\n<th>Applies to<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>taskTimeout<\/td>\n<td>60000 ms<\/td>\n<td>cy.task, so the solve<\/td>\n<\/tr>\n<tr>\n<td>responseTimeout<\/td>\n<td>30000 ms<\/td>\n<td>cy.request, so a raw API call<\/td>\n<\/tr>\n<tr>\n<td>defaultCommandTimeout<\/td>\n<td>4000 ms<\/td>\n<td>DOM commands, not either of the above<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Set it globally as in the config above, or per call when only one test needs the room:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ Same task, a longer leash for this one call.\ncy.task('solveRecaptcha', { sitekey, url }, { timeout: 180000 });<\/pre>\n<\/div>\n<p>180 seconds is a sensible ceiling. It is roughly ten times a normal solve, and it sits deliberately below the SDK&#8217;s own 300 second reCAPTCHA polling limit, so Cypress fails a genuinely stuck test rather than hanging behind a client that is still waiting. If you would rather the client give up first, lower recaptchaTimeout to something under your task timeout.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Or skip the SDK and use cy.request<\/h2>\n<p>The API is 2captcha compatible, so two calls do the whole job. Because cy.request runs in Node, the browser&#8217;s origin rules never enter into it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ No task registration needed. Both calls happen in Node.\nfunction pollForToken(id, tries = 20) {\n  return cy.request({\n    method: 'POST',\n    url: 'http:\/\/127.0.0.1:8080\/res.php',\n    form: true,\n    body: { key: 'capskip', action: 'get', id },\n  }).then((res) =&gt; {\n    const text = res.body.trim();\n    if (text !== 'CAPCHA_NOT_READY') return text.replace('OK|', '');\n    if (tries === 0) throw new Error('gave up waiting for ' + id);\n    return cy.wait(5000).then(() =&gt; pollForToken(id, tries - 1));\n  });\n}<\/pre>\n<\/div>\n<p>Two details to keep straight. The plain text reply from res.php is <code>OK|TOKEN<\/code> on success and the bare string CAPCHA_NOT_READY while the job is still running, which is a status and not an error. And a result is readable only once, so store it the moment it arrives rather than asking twice. Every parameter and every error string is listed in the <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the tests in CI while the solver stays put<\/h2>\n<p>This is where a suite that passes on your laptop fails on the first push. A GitHub Actions runner, a GitLab job or a Jenkins agent has its own loopback address, and nothing is listening on port 8080 there. Local mode is machine local by definition.<\/p>\n<p>Server mode is the answer. CapSkip listens on your network or public IP instead of loopback, and the config reads the address from the environment.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ npm install --save-dev capskip\nconst { CapSkip } = require('capskip');\n\n\/\/ Same client, different address. The spec never changes.\nconst solver = new CapSkip({\n  host: process.env.CAPSKIP_HOST || '127.0.0.1',\n  port: Number(process.env.CAPSKIP_PORT || 8080),\n});<\/pre>\n<\/div>\n<p>The SDK reads CAPSKIP_HOST and CAPSKIP_PORT on its own, so the fallback above is belt and braces for a runner that starts without them. A static public IP is recommended for the solver box, and the steps are in the <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>. It is still your hardware and still unmetered: the only thing that changed is where the process listens.<\/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>Symptom<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The task solveRecaptcha was not registered<\/td>\n<td>Registered in the wrong block, or the config never exported<\/td>\n<td>Register inside setupNodeEvents for the e2e key<\/td>\n<\/tr>\n<tr>\n<td>Timed out after waiting 60000ms for your task<\/td>\n<td>taskTimeout is still at its default<\/td>\n<td>Raise it to 180000, globally or per call<\/td>\n<\/tr>\n<tr>\n<td>The task returned undefined<\/td>\n<td>The handler has no return statement<\/td>\n<td>Return the token, or null when there is nothing<\/td>\n<\/tr>\n<tr>\n<td>Element is not visible, so Cypress cannot type<\/td>\n<td>The response field is a hidden textarea<\/td>\n<td>Write the value through cy.document instead<\/td>\n<\/tr>\n<tr>\n<td><code>ERROR_GOOGLEKEY<\/code><\/td>\n<td>The sitekey was empty, or belongs to a Turnstile widget<\/td>\n<td>Log the attribute before solving; Turnstile has its own method<\/td>\n<\/tr>\n<tr>\n<td>NetworkException on every test<\/td>\n<td>Nothing is listening at that host and port<\/td>\n<td>Local mode is loopback only; use Server mode from CI<\/td>\n<\/tr>\n<tr>\n<td>Green locally, red in CI<\/td>\n<td>The runner cannot reach your machine&#8217;s loopback<\/td>\n<td>Point CAPSKIP_HOST at a reachable address<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Frequently asked questions<\/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;\">Should I just turn the CAPTCHA off in my test environment?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">If the widget is yours, yes. A feature flag or a test sitekey in the staging build is cheaper and faster than solving, and it keeps the suite deterministic. Solving earns its place in three cases: the CAPTCHA belongs to somebody else, staging has to mirror production exactly, or the thing under test is the challenge path itself. The <a href=\"https:\/\/capskip.com\/captcha-demo\/\">CAPTCHA demo pages<\/a> are useful for the third one, because you can point a spec at a widget that behaves like the real thing.<\/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 in Cypress component testing?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Not usefully. A component test mounts a component with no real page and no server behind it, so a token has nothing to be verified against. Register the task under the component key if you want it available, but keep challenge work in end to end specs where there is a real request to submit.<\/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;\">Can a run recorded to Cypress Cloud reach the solver?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Cypress Cloud records results, it does not execute your tests, so the question is really about whichever machine runs the browser. On your laptop that is Local mode. On a hosted runner it needs Server mode and a route to the solver&#8217;s address, and the recording works the same either way.<\/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 many solves can a parallel Cypress run push at once?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">As many as you have spec files running. Each Cypress process holds its own client and awaits its own task, so there is nothing to configure on the client side. The SDK starts polling at 250 milliseconds and backs off to the pollingInterval ceiling, which keeps a fast solve fast even with several in flight. Because the work happens on hardware you own, adding machines is a capacity decision rather than a billing one.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Put the client in cypress.config.js, expose it as a task, raise taskTimeout to 180000, and write the token into the hidden field through cy.document. That is the whole integration, and the parts that break are the two Cypress rules underneath it: spec code is browser code, and a task returns or it fails.<\/p>\n<p>Running the solver yourself is what makes it reasonable to retry a flaky challenge instead of budgeting for it, and that argument holds wherever you put a <a href=\"https:\/\/capskip.com\/\">captcha solver<\/a>, in a test suite or in production. Client options are covered in detail in <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js integration guide<\/a>, and the browser-side details that Cypress shares with every other runner are in <a href=\"https:\/\/capskip.com\/playwright-captcha-solver\/\">the Playwright guide<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cypress \u7684\u6d4b\u8bd5\u7528\u4f8b\u8dd1\u5728\u6d4f\u89c8\u5668\u91cc\uff0c\u6240\u4ee5\u8bc6\u522b\u5de5\u5177\u7684\u5ba2\u6237\u7aef\u8981\u653e\u8fdb\u4e00\u4e2a Node \u4efb\u52a1\u3002\u672c\u6587\u8bf4\u660e\u8be5\u6ce8\u518c\u5728\u54ea\u91cc\u3001\u4f1a\u6740\u6b7b\u6574\u4e2a\u8fd0\u884c\u7684\u90a3\u4e2a\u8d85\u65f6\u8bbe\u7f6e\uff0c\u4ee5\u53ca\u5982\u4f55\u6ce8\u5165 token\u3002<\/p>","protected":false},"author":1,"featured_media":25304,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Cypress CAPTCHA: Solve It in a Node Task | CapSkip","rank_math_description":"A Cypress captcha cannot be solved inside the spec, because that code runs in a browser. Register a cy.task, raise taskTimeout, then inject the token.","rank_math_focus_keyword":"cypress captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25305","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\/25305","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=25305"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25305\/revisions"}],"predecessor-version":[{"id":25311,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25305\/revisions\/25311"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25304"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25305"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25305"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25305"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}