How to Solve GeeTest v3 in Node.js and Post It Back

solve geetest in node.js - How to Solve GeeTest v3 in Node.js and Post It Back

Almost every CAPTCHA integration assumes one token goes in and one comes out. GeeTest does not work that way. A solve returns three values that must be submitted together, and the challenge you started from expires about a minute after you fetched it. Neither failure produces a useful error message, which is why GeeTest has a reputation for being fiddly.

Two inputs, one of them perishable

ValueLifetime
gtStatic per site. Cache it freely
challengeSingle use, dead in roughly 60 seconds

Both come from the site’s own GeeTest init endpoint. Treating the challenge as reusable is the reason integrations pass in testing and collapse in production, where queueing delay pushes the solve past the expiry.

Setup

npm install capskip
const { CapSkip } = require('capskip');

const solver = new CapSkip({
  host: '127.0.0.1',
  port: 8080,
  recaptchaTimeout: 300,   // GeeTest uses this, not defaultTimeout
});

That distinction matters. defaultTimeout only governs image CAPTCHAs; everything interactive uses recaptchaTimeout.

Solving

const result = await solver.geetest(
  '81388ea1fc187e0c335c0a8907ff2625',   // gt, static per site
  '7cf6a8b1a2c34d5e6f7089abcdef0123',   // challenge, fetched seconds ago
  'https://example.com/login',
);

console.log(result.challenge, result.validate, result.seccode);

Those three properties are the answer. result.code is populated too, but for GeeTest it holds the raw JSON string rather than anything submittable, so reaching for it out of habit produces a baffling failure.

Use the challenge that comes back, not the one you passed in. They are not always identical.

The whole flow

const { CapSkip } = require('capskip');

const solver = new CapSkip();
const LOGIN = 'https://example.com/login';

// 1. Fresh pair, cache-busted. Cached init responses return spent challenges.
const init = await (await fetch(
  `https://example.com/geetest/init?t=${Date.now()}`,
)).json();

// 2. Solve immediately. Nothing slow between here and the previous step.
const result = await solver.geetest(init.gt, init.challenge, LOGIN);

// 3. Post all three together with the real form fields.
const response = await fetch(LOGIN, {
  method: 'POST',
  body: new URLSearchParams({
    geetest_challenge: result.challenge,
    geetest_validate: result.validate,
    geetest_seccode: result.seccode,
    username: '...',
    password: '...',
  }),
});

The cache-busting timestamp is doing real work. GeeTest init endpoints are frequently cached by a CDN or proxy, and a cached response hands you a challenge somebody already used.

Those field names are the usual GeeTest v3 convention, though a site can rename them. Check the real form before assuming.

The batching trap

This is where Node’s ergonomics work against you. The obvious parallel version is wrong:

// WRONG: every challenge is fetched up front, so the later solves
// start against pairs that have already expired.
const pairs = await Promise.all(urls.map(fetchPair));
const results = await Promise.all(
  pairs.map((p, i) => solver.geetest(p.gt, p.challenge, urls[i])));

Keep the fetch and the solve inside the same task so each pair is used within seconds of being issued:

// RIGHT: each task fetches its own pair immediately before solving.
const results = await Promise.all(urls.map(async (url) => {
  const pair = await fetchPair(url);
  return solver.geetest(pair.gt, pair.challenge, url);
}));

AsyncCapSkip is exported as an alias of CapSkip here, so there is no separate async client to reach for. Promise.all is the whole story.

Errors

const {
  ValidationException, NetworkException, ApiException, TimeoutException,
} = require('capskip');

try {
  const result = await solver.geetest(gt, challenge, pageUrl);
} catch (err) {
  if (err instanceof ValidationException)   { /* missing gt or challenge */ }
  else if (err instanceof NetworkException) { /* CapSkip not running */ }
  else if (err instanceof ApiException)     { /* usually an expired challenge */ }
  else if (err instanceof TimeoutException) { /* exceeded recaptchaTimeout */ }
  else throw err;
}

Most GeeTest failures arrive as ApiException and mean the challenge expired. Retrying with the same pair never works. Fetch a new one.

Frequently asked questions

Why can I not just use result.code?

Because GeeTest’s answer is three values. code holds the raw JSON for completeness while the SDK expands the useful parts into challenge, validate and seccode. Submit those.

Can I cache the gt value?

Yes. The gt is a site identifier and rarely changes. The challenge is the perishable half and must be fetched fresh every time.

Does this handle GeeTest v4?

The geetest method targets v3, the slide puzzle built on a gt and challenge pair. v4 changed the parameter model, so check the current API documentation before assuming the same call works.

Summary

Fetch a cache-busted pair, solve immediately, then post challenge, validate and seccode together. Use the returned challenge, cache only the gt, and keep fetch and solve inside the same task when running in parallel.

Other languages are on the GeeTest solver page, the wider Node surface on the Node.js CAPTCHA solver page, and there is a live puzzle on our GeeTest v3 demo. CapSkip does captcha bypass locally, so solve volume costs nothing.