Legibl docs PHP 0.1.0-dev · Python 0.1.0

Errors & outcomes

Three things can go wrong and they are kept apart on purpose: a thrown error means the SDK is unusable as configured; an outcome records what happened to one request; an API error code comes back from the service. Every one of them is listed here with its corrective action.

Typed errors — PHP

All three extend \RuntimeException, so one catch (\Legibl\Exception\LegiblException $e) clause catches everything the SDK raises and nothing else.

Legibl\Exception\ConfigurationException

The SDK is not usable as configured. Raised at first use rather than at some arbitrary later point, and the message always names the fix. There are four sites that raise it.

Unknown Legibl mode "…". Expected one of: live, stub, replay, dry_run.
Fix: LEGIBL_MODE holds a value outside that set — often a typo or a shell variable that expanded to empty plus something else. Set it to one of the four, or unset it and let the mode be inferred.
Legibl is in live mode but no API key is set. Set the LEGIBL_API_KEY environment variable, or use stub mode for local development.
Fix: you forced LEGIBL_MODE=live without LEGIBL_API_KEY. Set the key, or drop the explicit mode so the SDK falls back to stub. Common in containers where the key is injected into the app but not into a worker or cron process.
Legibl is in replay mode but no fixture corpus is configured. Set the LEGIBL_FIXTURES environment variable to the corpus directory.
Fix: point LEGIBL_FIXTURES at the corpus directory, or stop forcing LEGIBL_MODE=replay.
HttpSolver requires an API key. Set LEGIBL_API_KEY, or use stub mode.
Fix: you constructed HttpSolver directly with a Config that carries no key. Build the interceptor through Interceptor::fromConfig() instead, which selects a solver the mode actually supports.

Legibl\Exception\LegiblException

The base class, and raised directly in exactly one place: Legibl\curl_multi_exec().

Legibl does not wrap the curl multi-handle API, so challenge handling would be
silently inactive for these requests. Use the global \curl_multi_exec()
explicitly to acknowledge that, or route these requests through Legibl\curl_exec().

Fix: either call \curl_multi_exec() by its qualified global name — which acknowledges that those requests get no challenge handling — or move them onto Legibl\curl_exec(). The function refuses rather than forwarding silently, because a silent forward leaves the SDK looking installed while every challenge on those requests passes unhandled.

Legibl\Exception\SolveFailedException

Defined, and not thrown in this version

The class exists so that callers can distinguish a failed solve from a configuration problem, but no code path in PHP SDK 0.1.0-dev throws it. A challenge that cannot be solved is reported as the solve_failed outcome and the original response is returned. Do not write a catch block expecting it to fire; read the outcome instead.

Typed errors — Python

All three subclass RuntimeError, so except legibl.errors.LegiblError catches everything the SDK raises.

ErrorRaised whenCorrective action
ConfigurationError Config(...) with a mode outside live | stub | replay | dry_run. Correct LEGIBL_MODE, or unset it and let the mode be inferred.
ConfigurationError Mode live with no API key. Set LEGIBL_API_KEY, or stop forcing live mode.
ConfigurationError Mode replay with no fixture path. Set LEGIBL_FIXTURES to the corpus directory.
ConfigurationError HttpSolver(config) constructed with a keyless config. Use Interceptor.from_config(), which picks a solver the mode supports.
LegiblError Base class. Nothing raises it directly in 0.1.0. Catch it to catch all SDK errors at once.
SolveFailedError Never, in 0.1.0. A failed solve is the solve_failed outcome, not an exception. Read the outcome; do not depend on this being raised.
ImportError LegiblTransport() constructed without httpx installed. Message: httpx is not installed. Install it with: pip install 'legibl[httpx]' Install the extra, or use LegiblAdapter with requests instead.
NotImplementedError A custom Solver subclass that does not override solve(). Implement solve(challenge, page_url) -> Optional[Solution].

Outcomes

Every intercepted request produces exactly one outcome, delivered to your observer with the target domain and the elapsed time. These are the errors most customers will actually see, because the SDK's governing rule is graceful degradation: it returns your original response rather than raising.

OutcomeWhat it meansCorrective action
no_challenge No challenge the SDK recognises was found. Usually nothing. If you expected a challenge, confirm the type is supported — an unsupported system produces this outcome too.
solved A solution was obtained and the request resubmitted once with it attached. None. Note that “solved” means the token was submitted, not that the target accepted it; report acceptance back with reportCorrect / reportIncorrect.
unhandled A challenge was identified but is out of reach of form injection. Carries the captcha type and a reason. Read the reason and see unreachable challenges. This is a normal, expected outcome — not a bug.
unavailable The SDK could not act for reasons of its own: the body could not be inspected, or the request could not be reconstructed. Both causes are integration-side and both are fixable — see the fixes.
solve_failed The service did not return a usable solution: an error envelope, a transport failure, a solution with no token, or a timeout. Attach an observer and read the solveFailed message — it carries the service's errorCode. Match it against the error-code table.
expired A solution was obtained but exceeded the 110-second freshness window before it could be submitted. Carries solutionAge / solution_age. Look at your own latency between the solve and the resubmission — queueing, a slow retry wrapper, a debugger pause. Recorded separately so it is never charged to the solver's accuracy.
passthrough PHP only. The request failed, or its body went somewhere the SDK cannot see, so curl's own return value was handed straight back. Check curl_errno() / curl_error() on the handle. This is a transport failure, not a captcha failure.

Exact reason strings

The reason field is prose intended for a human reading a log. These are the strings this version emits.

OutcomeReasonMeaning
unavailable CURLOPT_RETURNTRANSFER is off or the body is routed to a callback or file, so the response cannot be inspected for a challenge Fix
unavailable the request cannot be reconstructed for resubmission; the handle was configured through the global curl_setopt rather than Legibl\curl_setopt Fix
unavailable the request cannot be reconstructed for resubmission Python: the integration reported that this request cannot be replayed.
unhandled recaptcha script present but no data-sitekey in server HTML; challenge is injected client-side Fix
unhandled turnstile script present but no cf-turnstile element in server HTML; widget is rendered explicitly in JavaScript Fix
unhandled cf-turnstile element present but carries no data-sitekey Fix
unhandled data-response-field="false": no hidden token field is created, so the token is consumed in JavaScript Fix
unhandled captcha image found but its input field is ambiguous; needs a per-target override Fix
unhandled no captcha-like image found Returned by the image extractor when the page has no image that looks like a captcha.
expired solution expired before resubmission See expired above.
passthrough response not returned as a string curl returned false or a bool; a transport failure.
Do not match on these strings

Reason strings are prose and are not contracted across languages — PHP and Python can currently explain the same unhandled challenge with different wording, and no test compares them. Switch on status and captchaType, which are protocol values; read reason with your eyes.

Solve-failure messages

When a solve does not happen, the solver calls solveFailed / solve_failed on your observer with one of four messages, suffixed with the service's errorCode when the response carried one.

MessageCorrective action
createTask failed: ERROR_KEY_DOES_NOT_EXIST The key is wrong, revoked, or absent. See invalid or missing API key.
createTask failed: ERROR_ZERO_BALANCE Top up. Note that card-purchased credit is held briefly before it becomes spendable.
createTask failed: ERROR_TASK_NOT_SUPPORTED The captcha type is not one of the four supported types.
createTask failed (no code) The service was unreachable, timed out, returned a non-2xx status, or returned something that was not JSON. Check network egress and LEGIBL_SERVICE_URL.
getTaskResult failed: … The task was accepted and then failed terminally — most often ERROR_CAPTCHA_UNSOLVABLE or ERROR_TIMEOUT. Retry the request; a repeated pattern on one target is worth reporting.
solution carried no usable token The result was ready but carried none of gRecaptchaResponse, token or text. This is a service-side defect — report it.
timed out waiting for a solution Polling exceeded LEGIBL_TIMEOUT (default 30s). Raise the timeout for reCAPTCHA v2 on a slow backend, or accept the miss. Measured p95 latencies for reCAPTCHA v2 run from 20s to 168s depending on the backend, so 30s does drop real traffic.

Service error codes

The full errorId / errorCode envelope and its code table are on the HTTP API page.