{"id":26658,"date":"2026-10-18T19:07:36","date_gmt":"2026-10-18T19:07:36","guid":{"rendered":"https:\/\/capskip.com\/?p=26658"},"modified":"2026-10-18T19:07:36","modified_gmt":"2026-10-18T19:07:36","slug":"solve-recaptcha-bun","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/solve-recaptcha-bun\/","title":{"rendered":"\u5982\u4f55\u5728 Bun \u4e2d\u8bc6\u522b reCAPTCHA\uff1a\u4f7f\u7528 .env \u6587\u4ef6\u548c bun test"},"content":{"rendered":"<p>To solve reCAPTCHA in Bun, add the CapSkip Node.js SDK with bun add. Build the client from values that Bun has already loaded from your .env file, then call the solver and post the token. CapSkip runs on your own Windows PC, so there is no per-solve bill. The code is short, and the SDK works under Bun as it is. Three Bun habits are not what a Node developer expects. Bun reads .env files without being asked, and only from the folder you run it in. A timeout on a hung socket rejects but does not end the process. And bun test stops every test after five seconds, which is shorter than a real solve. This guide covers each of them.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip running on a Windows PC, with the reCAPTCHA solver switched on. The <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a> walks through the first start.<\/li>\n<li>Bun for Windows. Everything below ran on Windows 11 with Bun 1.4.3 and capskip 1.3.0 from npm. Older Bun versions were not tested.<\/li>\n<li>A PowerShell prompt. The commands use PowerShell syntax.<\/li>\n<li>A page with a reCAPTCHA v2 checkbox in a form. The example logs in to a Django-style form that carries a CSRF token. Change the field names to match your site.<\/li>\n<li>An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a script on another machine can call it over the API. Both are set under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<p>The measurements below come from a mock CapSkip and a mock login page on the same PC. Bun and the SDK are the real ones. Bun and the SDK should behave the same against your own site, but only a mock was used. Solve times and the accept and reject rules of the login page are the mock&#8217;s own.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: install Bun<\/h2>\n<p>Bun&#8217;s own installation page gives the command below, and it also lists npm install -g bun. The tests for this guide used Bun 1.4.3 installed through npm into a project folder. The installer command itself was not run here. Open a new PowerShell window afterwards, so that PATH is refreshed.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># PowerShell: Bun's official installer (or: npm install -g bun)\npowershell -c &quot;irm bun.sh\/install.ps1|iex&quot;\n\n# Open a new PowerShell window so PATH is refreshed, then check:\nbun --version<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: create the project first<\/h2>\n<p>Run bun init before bun add. If the folder has no package.json, bun add looks upward for the nearest one and changes that file instead. On a test folder inside a decoy parent, it kept the parent&#8217;s other packages and added capskip to the parent. Your own parent folder could be a profile folder or a monorepo root. Creating the project file first keeps the install where you expect.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Make the folder, then create package.json BEFORE bun add.\nmkdir captcha-bun\ncd captcha-bun\nbun init -y\nbun add capskip<\/pre>\n<\/div>\n<p>The init command writes a strict tsconfig.json, an index.ts, a README.md, a .gitignore and a package.json with the module type set. It also installs TypeScript and the Bun type definitions. The add command saves capskip as version 1.3.0 or later and writes a text lockfile named bun.lock. The SDK is CommonJS. Both the named import used below and require work, which is different from Deno, where require fails.<\/p>\n<p>Skipping the init has a second cost. Bun resolves an import by walking up the folders. With no node_modules in your project, it loaded an older capskip 1.0.2 from a node_modules in a parent folder on the test PC. No error said so.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: put the settings in a .env file<\/h2>\n<p>Create a file called .env in the project folder. Bun loads it before your code runs, so the values are in Bun.env and process.env, and the two always agree. The SDK reads no environment variable itself. Your script reads the three values and passes them in, which is the next step. The LOGIN_URL line is for the live test further down.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># .env in the folder you run bun from (bun init already git-ignores it)\nCAPSKIP_HOST=127.0.0.1\nCAPSKIP_PORT=8080\nCAPSKIP_API_KEY=capskip\nLOGIN_URL=https:\/\/example.com\/login<\/pre>\n<\/div>\n<p>Know the rules, because a stray .env file can surprise you:<\/p>\n<ul>\n<li>Bun reads .env files only from the folder you run it in. Running a script by path from another folder reads nothing, and a .env one folder up is not read either.<\/li>\n<li>A variable set in the shell wins. Then come .env.local, then the file for the mode, which is .env.development under bun run unless NODE_ENV is set, and then plain .env.<\/li>\n<li>The bun init .gitignore covers .env and .env.local. It does not cover .env.production, .env.test or .env.development, so add them before you commit.<\/li>\n<li>Quotes, a trailing comment and ${OTHER} expansion work in the file.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: build the CapSkip client<\/h2>\n<p>Save the next blocks one after another in a file called solve.ts. Steps 4 to 9 all go into that one file, and the whole file is shown near the end. The defaults make the script run with no setup. CAPSKIP_API_KEY only matters if you switched on API key validation in CapSkip.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import {\n  ApiException,\n  CapSkip,\n  NetworkException,\n  TimeoutException,\n} from &quot;capskip&quot;;\n\n\/\/ Bun has already loaded .env into Bun.env. The SDK reads no\n\/\/ environment variable itself, so pass the three values in.\nconst solver = new CapSkip({\n  host: Bun.env.CAPSKIP_HOST ?? &quot;127.0.0.1&quot;,\n  port: Number(Bun.env.CAPSKIP_PORT ?? 8080),\n  apiKey: Bun.env.CAPSKIP_API_KEY ?? &quot;capskip&quot;,\n});<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: keep cookies yourself<\/h2>\n<p>The login form needs the cookie from the page that showed it, and fetch in Bun keeps no cookies. Read every Set-Cookie line with getSetCookie. The usual headers.get call joins the lines with a comma, and the comma inside an Expires date then breaks the split.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ fetch keeps no cookies, so collect every Set-Cookie line.\n\/\/ (headers.get(&quot;set-cookie&quot;) would glue them into one string.)\nconst jar = new Map&lt;string, string&gt;();\n\nfunction keepCookies(response: Response): void {\n  for (const line of response.headers.getSetCookie()) {\n    const pair = line.split(&quot;;&quot;)[0] ?? &quot;&quot;;\n    const eq = pair.indexOf(&quot;=&quot;);\n    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());\n  }\n}\n\nconst cookies = () =&gt;\n  Array.from(jar, ([name, value]) =&gt; `${name}=${value}`).join(&quot;; &quot;);<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 6: read the sitekey and the CSRF token<\/h2>\n<p>This part is yours, not CapSkip&#8217;s. It opens the login page and pulls out the two values. Adjust the patterns to your markup.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ Load the login page, keep its cookies, and read the sitekey and\n\/\/ the CSRF token. Adjust the two patterns to your page's markup.\nasync function loadForm(pageUrl: string) {\n  const page = await fetch(pageUrl);\n  keepCookies(page);\n  const html = await page.text();\n\n  const sitekey = html.match(\/data-sitekey=&quot;([^&quot;]+)&quot;\/)?.[1];\n  const csrf = html.match(\/name=&quot;csrfmiddlewaretoken&quot; value=&quot;([^&quot;]+)&quot;\/)?.[1];\n  if (!sitekey || !csrf) throw new Error(&quot;sitekey or CSRF token not found&quot;);\n  return { sitekey, csrf };\n}<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 7: add a timeout that works<\/h2>\n<p>The SDK sets no socket timeout. A CapSkip that accepts the call and never answers hangs the script. In testing, Bun printed nothing and never exited, and it was still waiting after 400 seconds. The SDK&#8217;s timeout option is checked between polls only, so it cannot help while a single request is stuck. The block below races the solve against a timer.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ The SDK sets no socket timeout, so a CapSkip that accepts the call\n\/\/ and never answers would hang the script. Race the solve against a\n\/\/ timer. In Bun an AbortSignal.timeout does not keep the process alive.\nfunction withTimeout&lt;T&gt;(work: Promise&lt;T&gt;, ms: number): Promise&lt;T&gt; {\n  const signal = AbortSignal.timeout(ms);\n  const limit = new Promise&lt;never&gt;((_, reject) =&gt; {\n    signal.addEventListener(&quot;abort&quot;, () =&gt;\n      reject(new TimeoutException(`no answer from CapSkip in ${ms} ms`)),\n    );\n  });\n  return Promise.race([work, limit]);\n}\n\n\/\/ The SDK gives up after 300 s by itself, so 330 s only fires when\n\/\/ CapSkip goes silent. code is typed &quot;string or object&quot;, hence String().\nasync function getToken(sitekey: string, pageUrl: string): Promise&lt;string&gt; {\n  const solving = solver.recaptcha(sitekey, pageUrl);\n  const { code } = await withTimeout(solving, 330_000);\n  return String(code);\n}<\/pre>\n<\/div>\n<p>Bun differs from Deno here, in your favour. A timer from AbortSignal.timeout does not keep Bun alive, so a normal solve still exits as soon as it is done. In testing, a script with a 330 second budget around a 1.8 second solve ended at about 1.8 seconds. A plain setTimeout that you never clear is a different matter. It kept Bun alive until it fired, 20.04 seconds for a 20 second budget. Clear such a timer in a finally block or use the signal.<\/p>\n<p>The catch is the other direction. When the budget runs out on a hung socket, the promise rejects at once, at 3.0 seconds in a test with a 3 second budget. Then Bun stays alive, because the stuck socket holds it open. That is why Step 9 ends with process.exit. The same happens when the race is lost while CapSkip is still answering not ready. The SDK keeps polling in the background, and the process lives until the SDK&#8217;s own timeout, which was 12.8 seconds with a limit of 12.<\/p>\n<p>Pick a smaller budget if your script should fail faster. The tests ran the block with 4 seconds and a CapSkip that went silent on the submit, and then on the poll. Both printed the message from Step 9 and exited with code 1 at about 4.04 seconds. A normal solve exited with code 0 at about 1.8 seconds.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 8: solve, post and check the result<\/h2>\n<p>The next block is the login. A reCAPTCHA token works once, so each login solves its own. Two details matter. A strict site such as Django over HTTPS rejects a form post that has no Referer, and fetch sends none, so the block sets it. And fetch follows a 302 by default and drops the new cookie on the way. So the block sets redirect to manual and reads the cookie from the 302. Change the field names, the username and the Welcome back marker to match your site.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ fetch sends no Referer, and a strict site rejects the post without\n\/\/ one. redirect &quot;manual&quot; keeps the 302 so we can read its new cookie.\nexport async function login(pageUrl: string): Promise&lt;boolean&gt; {\n  jar.clear();\n  const { sitekey, csrf } = await loadForm(pageUrl);\n  const token = await getToken(sitekey, pageUrl);\n\n  const response = await fetch(pageUrl, {\n    method: &quot;POST&quot;,\n    redirect: &quot;manual&quot;,\n    headers: { Cookie: cookies(), Referer: pageUrl },\n    body: new URLSearchParams({\n      csrfmiddlewaretoken: csrf,\n      username: &quot;YOUR_USERNAME&quot;,\n      password: &quot;YOUR_PASSWORD&quot;,\n      &quot;g-recaptcha-response&quot;: token,\n    }),\n  });\n  keepCookies(response);\n  if (response.status !== 302) {\n    const body = await response.text();\n    throw new Error(`login rejected: ${response.status} ${body}`);\n  }\n\n  \/\/ Swap &quot;Welcome back&quot; for a marker your own site shows after login.\n  const next = new URL(response.headers.get(&quot;location&quot;) ?? &quot;\/&quot;, pageUrl);\n  const home = await fetch(next, { headers: { Cookie: cookies() } });\n  return (await home.text()).includes(&quot;Welcome back&quot;);\n}<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 9: report errors and run it<\/h2>\n<p>A closed port, an error from CapSkip and a slow solve are three different exception classes, and instanceof tells them apart under Bun. A closed port raised NetworkException in about 10 milliseconds with the message connect ECONNREFUSED 127.0.0.1 and the port. The host name localhost was nearly as fast but left the message empty, with the error code on the cause. The block falls back to that code.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\/\/ A closed port, a bad reply and a slow solve are three different\n\/\/ exception classes. A host NAME can leave the message empty, so fall\n\/\/ back to the error code on .cause.\nfunction explain(err: unknown): string {\n  if (err instanceof NetworkException) {\n    const code = (err.cause as { code?: string } | undefined)?.code;\n    return `CapSkip is not reachable: ${err.message || code}`;\n  }\n  if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;\n  if (err instanceof TimeoutException) return `Too slow: ${err.message}`;\n  return err instanceof Error ? (err.stack ?? err.message) : String(err);\n}\n\n\/\/ import.meta.main is true only when this file is run directly, so\n\/\/ the tests can import login() without starting a login.\nif (import.meta.main) {\n  try {\n    const ok = await login(Bun.argv[2] ?? &quot;https:\/\/example.com\/login&quot;);\n    console.log(ok ? &quot;logged in&quot; : &quot;NOT logged in&quot;);\n    process.exit(ok ? 0 : 2);\n  } catch (err) {\n    console.error(explain(err));\n    \/\/ A hung socket would keep Bun alive, so leave explicitly.\n    process.exit(1);\n  }\n}<\/pre>\n<\/div>\n<p>Replace the example URL with your own login page, and run it from the project folder. Bun picks up the .env file there. A variable set in PowerShell beats the same name in the file, and it stays set for the rest of that PowerShell session, so the block clears it again. The last two commands switch the automatic loading off, or read a file of your choosing instead. Create that file first. A missing file is skipped without a message, and .env is not read either.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Run it. Bun reads .env from the folder you run this in.\nbun run solve.ts https:\/\/example.com\/login\n\n# A variable set in PowerShell beats the same name in .env.\n$env:CAPSKIP_API_KEY = &quot;my-key&quot;\nbun run solve.ts https:\/\/example.com\/login\nRemove-Item Env:CAPSKIP_API_KEY\n\n# Ignore .env completely, or read a different file instead. The second\n# file must exist: a missing one is skipped and .env is not read either.\nbun run --no-env-file solve.ts https:\/\/example.com\/login\nbun run --env-file=.env.office solve.ts https:\/\/example.com\/login<\/pre>\n<\/div>\n<p>A good run prints logged in and exits with code 0. Against the mock, a solve that the mock held for one second took 1.77 seconds. The SDK polls at once, then waits 0.25, 0.5, 1, 2 and 4 seconds, and 5 seconds after that. A short solve is not rounded up to a flat five.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Test it with bun test<\/h2>\n<p>Bun ships its own test runner. It runs files named like solve.test.ts, and its default limit is 5000 milliseconds per test. A real reCAPTCHA solve can take longer. A test that waited for the 8 second mock failed with this, and the run exited with code 1:<\/p>\n<ul>\n<li>(fail) logs in through CapSkip [5001.15ms]<\/li>\n<li>this test timed out after 5000ms.<\/li>\n<\/ul>\n<p>Raise the limit in one of three ways: the third argument of test, setDefaultTimeout as below, or the timeout switch of bun test. All three passed against the 8 second mock. A timeout key under the test heading of bunfig.toml did not work. The test still failed at 5000 milliseconds, and Bun&#8217;s bunfig documentation lists no such key. The live test reads LOGIN_URL from the .env file, so set it to your login page. Save this one as solve.test.ts.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import { expect, setDefaultTimeout, test } from &quot;bun:test&quot;;\nimport { login } from &quot;.\/solve&quot;;\n\n\/\/ bun test stops every test after 5 s, and a real solve can take\n\/\/ longer. Raise the limit for the whole file (or pass a number as the\n\/\/ third argument of test(), or run: bun test --timeout 120000).\nsetDefaultTimeout(120_000);\n\ntest(&quot;logs in through CapSkip&quot;, async () =&gt; {\n  const ok = await login(Bun.env.LOGIN_URL ?? &quot;https:\/\/example.com\/login&quot;);\n  expect(ok).toBe(true);\n});<\/pre>\n<\/div>\n<p>Most of your suite should not need a solver at all. The second test replaces the SDK with a fake and runs against a small stand-in site, so it passes with CapSkip closed. Two things are easy to get wrong. The import of solve.ts must be dynamic, because a static import runs before mock.module does. And the fake leaks. The mock stays in force for the other test files. Plain bun test made solve.test.ts fail with login rejected: 403, because it was now using the fake solver. Save this one as fake.test.ts.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import { expect, mock, test } from &quot;bun:test&quot;;\n\n\/\/ Replace the SDK before solve.ts loads it. The import must be dynamic,\n\/\/ because a static import runs before mock.module() does.\nmock.module(&quot;capskip&quot;, () =&gt; ({\n  CapSkip: class {\n    async recaptcha() {\n      return { captchaId: &quot;1&quot;, code: &quot;fake-token&quot; };\n    }\n  },\n  ApiException: class extends Error {},\n  NetworkException: class extends Error {},\n  TimeoutException: class extends Error {},\n}));\nconst { login } = await import(&quot;.\/solve&quot;);\n\n\/\/ A tiny stand-in login site: it only accepts the fake token.\nconst site = Bun.serve({\n  port: 0,\n  async fetch(req) {\n    const url = new URL(req.url);\n    if (req.method === &quot;GET&quot; &amp;&amp; url.pathname === &quot;\/login&quot;) {\n      return new Response(\n        '&lt;div data-sitekey=&quot;K&quot;&gt;&lt;\/div&gt;' +\n          '&lt;input name=&quot;csrfmiddlewaretoken&quot; value=&quot;C&quot;&gt;',\n      );\n    }\n    if (req.method === &quot;POST&quot;) {\n      const form = await req.formData();\n      if (form.get(&quot;g-recaptcha-response&quot;) !== &quot;fake-token&quot;)\n        return new Response(&quot;bad token&quot;, { status: 403 });\n      return new Response(null, {\n        status: 302,\n        headers: { Location: &quot;\/home&quot; },\n      });\n    }\n    return new Response(&quot;Welcome back&quot;);\n  },\n});\n\ntest(&quot;login posts the token CapSkip returned&quot;, async () =&gt; {\n  expect(await login(`http:\/\/127.0.0.1:${site.port}\/login`)).toBe(true);\n  await site.stop();\n});<\/pre>\n<\/div>\n<p>The fix is the isolate switch, which gives each file a fresh global object. With it, both files passed. Bun also sets NODE_ENV to test during bun test, so it loads .env.test and skips .env.local.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Run every *.test.ts file. --isolate gives each file a fresh global\n# object, so the fake SDK in fake.test.ts cannot leak into the others.\nbun test --isolate\n\n# The default limit is 5 s per test. Raise it for all tests (ms).\nbun test --isolate --timeout 120000<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Build one .exe<\/h2>\n<p>The compile command puts the Bun runtime, your script and the SDK in a single file. It took 0.3 seconds here and made a file of 96.4 MB, the size of Bun itself. The exe ran with a PATH that had no Bun on it. The .env file is not baked in. The exe reads the .env of the folder it runs from, at run time. It does not use the one from the build folder or the one next to the exe. Started from a folder with no .env, the port comes back undefined, and the defaults in Step 4 apply. In Task Scheduler, set the Start in folder to the one with your .env. Add the no-compile-autoload-dotenv switch to the build command and the exe ignores the file. Real variables still work.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># One .exe with the Bun runtime and the SDK inside. Run it from the\n# folder that holds your .env (Task Scheduler: set &quot;Start in&quot;).\nbun build --compile --outfile solve.exe solve.ts\n.\\solve.exe https:\/\/example.com\/login<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Check the types on demand<\/h2>\n<p>The bun run command strips types and never checks them. A file with a type error still ran and logged in. Bun 1.4.3 has a bun check command that runs TypeScript over the project, and both test files were clean. Three typing details of the SDK are worth knowing. The code field is declared as a string or an object, which is why Step 7 wraps it in String. The token field is optional. And the options type of the recaptcha call accepts any name, so a misspelled option compiles. A misspelled constructor option such as hots is an error.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># bun run never type checks. Check on demand (TypeScript from bun init):\nbun check<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Run the script against CapSkip on another machine<\/h2>\n<p>The script and the solver do not have to share a PC. Switch CapSkip to Server mode under connection settings, and it listens on your network address or public IP. Put that address in the .env of the machine that runs the script. A static public IP is recommended for stable remote access. The solver is still your own Windows machine, and solves are still unmetered.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># .env on the PC that runs the script, with CapSkip in Server mode\n# on another PC. Use that PC's address, and an API key if you set one.\nCAPSKIP_HOST=192.168.1.50\nCAPSKIP_PORT=8080\nCAPSKIP_API_KEY=your-long-random-key<\/pre>\n<\/div>\n<p>Mind two things. The SDK talks to CapSkip over plain HTTP, so the API key travels in clear text. Keep that link on a private network or a VPN, and turn on API key validation. And CapSkip opens its port to every address when Server mode starts. To limit access to the machines you expect, add a Windows Firewall block rule for the others. The remote setup was not run for this guide. Only the .env file was parsed.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The whole solve.ts<\/h2>\n<p>This is Steps 4 to 9 in one file, for a quick start. If you saved the blocks one by one, you already have it. Do not paste the file twice, or Bun reports that a name is already declared.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">import {\n  ApiException,\n  CapSkip,\n  NetworkException,\n  TimeoutException,\n} from &quot;capskip&quot;;\n\n\/\/ Bun has already loaded .env into Bun.env. The SDK reads no\n\/\/ environment variable itself, so pass the three values in.\nconst solver = new CapSkip({\n  host: Bun.env.CAPSKIP_HOST ?? &quot;127.0.0.1&quot;,\n  port: Number(Bun.env.CAPSKIP_PORT ?? 8080),\n  apiKey: Bun.env.CAPSKIP_API_KEY ?? &quot;capskip&quot;,\n});\n\n\/\/ fetch keeps no cookies, so collect every Set-Cookie line.\n\/\/ (headers.get(&quot;set-cookie&quot;) would glue them into one string.)\nconst jar = new Map&lt;string, string&gt;();\n\nfunction keepCookies(response: Response): void {\n  for (const line of response.headers.getSetCookie()) {\n    const pair = line.split(&quot;;&quot;)[0] ?? &quot;&quot;;\n    const eq = pair.indexOf(&quot;=&quot;);\n    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());\n  }\n}\n\nconst cookies = () =&gt;\n  Array.from(jar, ([name, value]) =&gt; `${name}=${value}`).join(&quot;; &quot;);\n\n\/\/ Load the login page, keep its cookies, and read the sitekey and\n\/\/ the CSRF token. Adjust the two patterns to your page's markup.\nasync function loadForm(pageUrl: string) {\n  const page = await fetch(pageUrl);\n  keepCookies(page);\n  const html = await page.text();\n\n  const sitekey = html.match(\/data-sitekey=&quot;([^&quot;]+)&quot;\/)?.[1];\n  const csrf = html.match(\/name=&quot;csrfmiddlewaretoken&quot; value=&quot;([^&quot;]+)&quot;\/)?.[1];\n  if (!sitekey || !csrf) throw new Error(&quot;sitekey or CSRF token not found&quot;);\n  return { sitekey, csrf };\n}\n\n\/\/ The SDK sets no socket timeout, so a CapSkip that accepts the call\n\/\/ and never answers would hang the script. Race the solve against a\n\/\/ timer. In Bun an AbortSignal.timeout does not keep the process alive.\nfunction withTimeout&lt;T&gt;(work: Promise&lt;T&gt;, ms: number): Promise&lt;T&gt; {\n  const signal = AbortSignal.timeout(ms);\n  const limit = new Promise&lt;never&gt;((_, reject) =&gt; {\n    signal.addEventListener(&quot;abort&quot;, () =&gt;\n      reject(new TimeoutException(`no answer from CapSkip in ${ms} ms`)),\n    );\n  });\n  return Promise.race([work, limit]);\n}\n\n\/\/ The SDK gives up after 300 s by itself, so 330 s only fires when\n\/\/ CapSkip goes silent. code is typed &quot;string or object&quot;, hence String().\nasync function getToken(sitekey: string, pageUrl: string): Promise&lt;string&gt; {\n  const solving = solver.recaptcha(sitekey, pageUrl);\n  const { code } = await withTimeout(solving, 330_000);\n  return String(code);\n}\n\n\/\/ fetch sends no Referer, and a strict site rejects the post without\n\/\/ one. redirect &quot;manual&quot; keeps the 302 so we can read its new cookie.\nexport async function login(pageUrl: string): Promise&lt;boolean&gt; {\n  jar.clear();\n  const { sitekey, csrf } = await loadForm(pageUrl);\n  const token = await getToken(sitekey, pageUrl);\n\n  const response = await fetch(pageUrl, {\n    method: &quot;POST&quot;,\n    redirect: &quot;manual&quot;,\n    headers: { Cookie: cookies(), Referer: pageUrl },\n    body: new URLSearchParams({\n      csrfmiddlewaretoken: csrf,\n      username: &quot;YOUR_USERNAME&quot;,\n      password: &quot;YOUR_PASSWORD&quot;,\n      &quot;g-recaptcha-response&quot;: token,\n    }),\n  });\n  keepCookies(response);\n  if (response.status !== 302) {\n    const body = await response.text();\n    throw new Error(`login rejected: ${response.status} ${body}`);\n  }\n\n  \/\/ Swap &quot;Welcome back&quot; for a marker your own site shows after login.\n  const next = new URL(response.headers.get(&quot;location&quot;) ?? &quot;\/&quot;, pageUrl);\n  const home = await fetch(next, { headers: { Cookie: cookies() } });\n  return (await home.text()).includes(&quot;Welcome back&quot;);\n}\n\n\/\/ A closed port, a bad reply and a slow solve are three different\n\/\/ exception classes. A host NAME can leave the message empty, so fall\n\/\/ back to the error code on .cause.\nfunction explain(err: unknown): string {\n  if (err instanceof NetworkException) {\n    const code = (err.cause as { code?: string } | undefined)?.code;\n    return `CapSkip is not reachable: ${err.message || code}`;\n  }\n  if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;\n  if (err instanceof TimeoutException) return `Too slow: ${err.message}`;\n  return err instanceof Error ? (err.stack ?? err.message) : String(err);\n}\n\n\/\/ import.meta.main is true only when this file is run directly, so\n\/\/ the tests can import login() without starting a login.\nif (import.meta.main) {\n  try {\n    const ok = await login(Bun.argv[2] ?? &quot;https:\/\/example.com\/login&quot;);\n    console.log(ok ? &quot;logged in&quot; : &quot;NOT logged in&quot;);\n    process.exit(ok ? 0 : 2);\n  } catch (err) {\n    console.error(explain(err));\n    \/\/ A hung socket would keep Bun alive, so leave explicitly.\n    process.exit(1);\n  }\n}<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors when you solve reCAPTCHA in Bun<\/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>this test timed out after 5000ms<\/td>\n<td>bun test cuts every test at five seconds<\/td>\n<td>setDefaultTimeout, or a third argument to test<\/td>\n<\/tr>\n<tr>\n<td>login rejected: 403 in solve.test.ts only<\/td>\n<td>mock.module in another file is still in force<\/td>\n<td>Run bun test with the isolate switch<\/td>\n<\/tr>\n<tr>\n<td>NetworkException with an empty message<\/td>\n<td>The host is a name such as localhost, so the error sits on the cause<\/td>\n<td>Print err.cause.code, or use 127.0.0.1<\/td>\n<\/tr>\n<tr>\n<td>NetworkException: connect ECONNREFUSED 127.0.0.1<\/td>\n<td>CapSkip is not running, or the port is wrong<\/td>\n<td>Start CapSkip and check the port in connection settings<\/td>\n<\/tr>\n<tr>\n<td>Object is not a constructor<\/td>\n<td>A default import gives the whole exports object<\/td>\n<td>Import CapSkip by name, in curly braces<\/td>\n<\/tr>\n<tr>\n<td>The script hangs and prints nothing<\/td>\n<td>CapSkip accepted the call and went silent<\/td>\n<td>Use the withTimeout race and end with process.exit<\/td>\n<\/tr>\n<tr>\n<td>The wrong capskip version loads<\/td>\n<td>No node_modules in the project, so Bun found an older copy higher up<\/td>\n<td>Run bun init and bun add inside the project folder<\/td>\n<\/tr>\n<tr>\n<td>Settings come back undefined in the exe<\/td>\n<td>The exe reads the .env of its working folder<\/td>\n<td>Run it from that folder, or set Start in<\/td>\n<\/tr>\n<tr>\n<td>fetch to 127.0.0.1 fails with ECONNREFUSED<\/td>\n<td>HTTP_PROXY points at a proxy that is down, and fetch uses it<\/td>\n<td>Set NO_PROXY to 127.0.0.1,localhost<\/td>\n<\/tr>\n<tr>\n<td>ApiException: ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>CapSkip could not solve this one<\/td>\n<td>Retry the solve<\/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;\">Does the CapSkip Node.js SDK work in Bun?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, on Bun 1.4.3 with capskip 1.3.0. The import, a full solve, the four exception classes and the polling all behaved as they do in Node. The solve took about 1.8 seconds in both Bun 1.4.3 and Node 25 against the same mock. For the plain Node version of this setup, see the guide to <a href=\"https:\/\/capskip.com\/recaptcha-v2-nodejs\/\">solving reCAPTCHA v2 in Node.js<\/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 is this different from Deno?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Deno blocks network and file access until you allow it, and an AbortSignal timer keeps Deno alive. Bun has no permission flags, so there is nothing to allow. The traps are the automatic .env loading, the hung socket that survives a timeout, and the five second test limit. The <a href=\"https:\/\/capskip.com\/solve-recaptcha-deno\/\">Deno guide<\/a> covers the permission side.<\/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 I run several solves at once?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, with Promise.all over several solver calls. In the Node SDK, AsyncCapSkip is only another name for CapSkip, so it adds nothing over the plain client. Mind CapSkip&#8217;s own limit: the reCAPTCHA Max. Threads setting defaults to 10, and extra jobs wait for a free thread. Concurrency was not measured under Bun for this guide.<\/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 script on a hosted platform use my CapSkip?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">A hosted platform runs on someone else&#8217;s machine, so it cannot reach 127.0.0.1 on your PC. Run CapSkip in Server mode on a Windows machine you control, with a static public IP. Put that address in the .env or in the platform&#8217;s secret store. CapSkip already allows every address on its port, so add a block rule for the rest, and remember that the key travels in clear text over plain HTTP. No hosted platform was tested for this guide.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>To solve reCAPTCHA in Bun, run bun init before bun add. Keep the CapSkip settings in a .env file and pass them to the client yourself. Race every solve against a timer, and end the script with process.exit so that a hung socket cannot keep it alive. Raise the five second test limit, and keep a fake-solver test beside the live one.<\/p>\n<ul>\n<li>Every CAPTCHA type the Node.js package handles: <a href=\"https:\/\/capskip.com\/nodejs-captcha-solver\/\">the Node.js CAPTCHA solver page<\/a>.<\/li>\n<li>How the checkbox solver works: <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<li>The same task on another runtime: <a href=\"https:\/\/capskip.com\/solve-recaptcha-deno\/\">solving reCAPTCHA in Deno<\/a>.<\/li>\n<\/ul>\n<p>A script that solves a few hundred times a night adds up quickly. Running it through an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> on your own Windows machine means those solves never turn into a bill.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Bun \u4f1a\u81ea\u52a8\u52a0\u8f7d\u4f60\u7684 .env \u6587\u4ef6\uff0c\u4f1a\u5728\u4e94\u79d2\u540e\u4e2d\u65ad\u6bcf\u4e2a\u6d4b\u8bd5\uff0c\u5e76\u4e14\u5728\u5957\u63a5\u5b57\u6302\u8d77\u5bfc\u81f4\u8d85\u65f6\u4e4b\u540e\u4ecd\u7136\u4fdd\u6301\u8fd0\u884c\u3002\u672c\u6587\u7ed9\u51fa\u4e00\u4e2a Bun \u811a\u672c\uff1a\u5b83\u7528 CapSkip \u8bc6\u522b reCAPTCHA\uff0c\u5b8c\u6210\u767b\u5f55\uff0c\u5e76\u80fd\u5728\u771f\u5b9e\u8bc6\u522b\u670d\u52a1\u6216\u6a21\u62df\u670d\u52a1\u4e0b\u901a\u8fc7 bun test\u3002<\/p>","protected":false},"author":1,"featured_media":26657,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Solve reCAPTCHA in Bun: .env, Timeouts, bun test | CapSkip","rank_math_description":"Learn to solve reCAPTCHA in Bun with the CapSkip Node.js SDK: load the .env file, add a timeout that works and test with bun test. Copy the script.","rank_math_focus_keyword":"solve recaptcha in bun","footnotes":""},"categories":[70],"tags":[],"class_list":["post-26658","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\/26658","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=26658"}],"version-history":[{"count":3,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26658\/revisions"}],"predecessor-version":[{"id":26665,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26658\/revisions\/26665"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/26657"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=26658"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=26658"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=26658"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}