Error Handling
Errors in the API come in two layers, and you handle them very differently:
| Layer | Where to look | Meaning | Examples |
|---|---|---|---|
| Request | ret in the response body is not 1 |
This HTTP request wasn’t accepted — no task was created and nothing was charged | Invalid parameters, insufficient credits, invalid API key |
| Task | The task’s status is failed or canceled, with the reason in error_code |
The task was created but didn’t produce an answer — nothing is charged | Platform crawl failed, API key invalidated before dispatch |
Request errors
Section titled “Request errors”Response format
Section titled “Response format”When a request fails, the HTTP status code is still 200, and the body looks like this:
{ "ret": 1023, "msg": "{\"zh\":\"点数余额不足,请充值后重试\",\"en\":\"Insufficient credits. Please top up and try again.\"}"}- Always branch on
retin code. Message wording may change, so don’t build logic on it. msgis a JSON string. Parsing it gives you{"zh": "…", "en": "…"}— the message in Chinese and English — which you can show to your users. To be safe, fall back to the rawmsgif parsing fails.- The message for invalid parameters (
1001) names the offending field, for example “Invalid parameters: query must be at most 4096 characters”.
Error codes
Section titled “Error codes”ret |
Meaning | Can occur on | Retry? | What to do |
|---|---|---|---|---|
1 |
Success | All | — | — |
1001 |
Invalid parameters: the body isn’t a single JSON object, exceeds 64 KiB, contains undefined fields, or has an invalid value | All | No | Fix the request as the message says. Field names are snake_case, e.g. task_id, wait_seconds |
1022 |
Developer API billing isn’t enabled | Submit a Task | No | Contact sales to enable it |
1023 |
Insufficient credits | Submit a Task | After topping up | Top up, or switch to a cheaper platform / P2 |
1024 |
Too many tasks waiting for dispatch or running | Submit a Task | Yes, later | Wait for some tasks to finish, then submit again. See Rate Limits & Quotas |
1025 |
Too many submissions | Submit a Task | Yes, later | Slow down and retry with backoff |
1029 |
Platform unsupported or under maintenance | Submit a Task | With another platform | Fetch the platform list again and pick an available platform |
1030 |
API key missing, invalid, disabled, or expired | All | No | Check your key — see Authentication errors |
1031 |
Task not found | Retrieve a Task | No | Check the task_id; you can only retrieve tasks under your own account |
1003 |
No permission (account permission issue, e.g. ownership of the key’s account was transferred) | All | No | Rare — please contact us |
-1 |
Service temporarily unavailable | All | Yes, with backoff | Retry with exponential backoff. On submit, first confirm whether the task was created — see below |
Request errors (except -1) all happen before a task is created: a rejected submission creates no task and charges no credits.
Non-200 HTTP status codes
Section titled “Non-200 HTTP status codes”405 (the method isn’t POST) and 404 (wrong path) mean the request never reached the business logic, so no task was created — fix your code and send it again.
Otherwise, if you get a non-200 status such as 502 or 504, or the connection times out or drops, something went wrong on the network path and you can’t tell whether the server processed the request:
- Read endpoints (List Platforms, Retrieve a Task, List Tasks): simply retry.
- Submit a Task: follow the steps in “When a submission’s outcome is unknown” below before deciding whether to submit again.
Task failures: error_code
Section titled “Task failures: error_code”When a task’s status is failed or canceled, error_code explains why. Failed and canceled tasks are never charged; the reserved credits are released.
error_code |
status |
Meaning | What to do |
|---|---|---|---|
crawl_failed |
failed |
The platform didn’t return a usable answer (timeout, platform error, etc.) | Retry later or switch platforms; if timing isn’t critical, use P2, which waits in the queue while the platform is unavailable |
credential_invalid |
failed |
The API key that submitted the task was disabled, deleted, or expired before dispatch | Resubmit with a valid key |
crawler_rejected |
failed |
The platform isn’t supported or has been retired | Fetch the platform list again and switch platforms |
crawler_internal_error |
failed |
The crawler hit an internal error and didn’t accept the task | Retry later |
crawler_submit_failed |
failed |
The crawler didn’t accept the task | Retry later |
crawler_invalid_request |
failed |
The crawler rejected the request parameters | Check the query and country, then resubmit |
crawler_method_unavailable |
failed |
The crawler hasn’t enabled raw-result crawling (a server-side configuration issue) | Please contact us |
crawler_auth_failed |
failed |
A server-side configuration issue | Please contact us |
crawl_canceled |
canceled |
The task was canceled | Resubmit if you still need it |
These two values appear on unfinished tasks and don’t indicate failure:
error_code |
status |
Meaning |
|---|---|---|
dispatch_uncertain |
dispatching |
The dispatch outcome is being confirmed. The server confirms it on its own and never dispatches twice — keep polling |
crawler_query_unavailable |
Unfinished | Progress temporarily can’t be synced. Keep polling; contact us if it doesn’t change for a long time |
error_message uses the same format as msg on request errors: a JSON string that parses to {"zh": "…", "en": "…"}. Log it, or show the message in your user’s language; if parsing fails, use the raw string. Don’t build logic on the wording — branch on error_code. raw_result.error (and error inside result_json) use the same format.
Retry strategy
Section titled “Retry strategy”| Situation | What to do |
|---|---|
-1, network errors, non-200 status (read endpoints) |
Retry with exponential backoff, e.g. 2, 4, 8 seconds…, a few times at most |
1024, 1025 |
No task was created, so retrying after a wait is safe; also lower your concurrency and submission rate |
-1, timeout, dropped connection (Submit a Task) |
Don’t retry right away — first confirm whether the task was created (see the next section) |
1001, 1003, 1022, 1023, 1029, 1030, 1031 |
Don’t retry automatically; fix the problem first |
Task failed / canceled |
Resubmitting creates a new task, charged only if it succeeds; for crawl_failed, wait a while or switch platforms |
When a submission’s outcome is unknown
Section titled “When a submission’s outcome is unknown”If a submit request times out, the connection drops, or it returns -1, the task may already have been created. Retrying blindly creates a second task — and if both succeed, you’re charged twice. Do this instead:
- Call List Tasks with the question text as
keyword(up to 256 characters, matched as “contains” — for a longer question, pass its first 256 characters) andstart_timeset to just before the submission. - Found it: keep polling that
task_id. - Not found: submit again.
# BASE_URL and HEADERS are defined as in the Quickstartimport jsonimport time
import requests
RETRYABLE = {-1, 1024, 1025}
class MaxAEOError(Exception): def __init__(self, ret, msg): self.ret = ret try: self.message = json.loads(msg).get("en") or msg except (TypeError, ValueError, AttributeError): self.message = msg super().__init__(f"ret={ret}: {self.message}")
def post(path, body): resp = requests.post(BASE_URL + path, headers=HEADERS, json=body, timeout=30) resp.raise_for_status() data = resp.json() if data["ret"] != 1: raise MaxAEOError(data["ret"], data.get("msg", "")) return data["data"]["ent"]
def get_with_retry(path, body, retries=4): """Read endpoints: safe to retry.""" for attempt in range(retries + 1): try: return post(path, body) except (requests.RequestException, MaxAEOError) as e: retryable = not isinstance(e, MaxAEOError) or e.ret in RETRYABLE if not retryable or attempt == retries: raise time.sleep(2 ** (attempt + 1))
def submit_safely(platform, query): """Submit a task, resubmitting only once we've confirmed none was created.""" started = int(time.time()) - 60 # allow for clock skew for attempt in range(4): try: return post("/task/submit", {"platform": platform, "query": query}) except MaxAEOError as e: if e.ret in (1024, 1025): # no task was created; wait and try again time.sleep(15 * (attempt + 1)) continue if e.ret != -1: raise except requests.RequestException: pass # Outcome unknown: look for the submission we just made found = get_with_retry("/task/key/list", { "keyword": query.strip()[:256], "platform": platform, "start_time": started, "page_size": 5, }) if found["items"]: return get_with_retry("/task/get", {"task_id": found["items"][0]["task_id"]}) raise RuntimeError("Submission failed after several attempts; try again later")// BASE_URL and headers are defined as in the Quickstartconst RETRYABLE = new Set([-1, 1024, 1025]);
class MaxAEOError extends Error { constructor(ret, msg) { let message = msg; try { message = JSON.parse(msg).en || msg; } catch {} super(`ret=${ret}: ${message}`); this.ret = ret; }}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function post(path, body) { const resp = await fetch(BASE_URL + path, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(30_000), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); if (data.ret !== 1) throw new MaxAEOError(data.ret, data.msg ?? ""); return data.data.ent;}
// Read endpoints: safe to retryasync function getWithRetry(path, body, retries = 4) { for (let attempt = 0; ; attempt++) { try { return await post(path, body); } catch (e) { const retryable = !(e instanceof MaxAEOError) || RETRYABLE.has(e.ret); if (!retryable || attempt === retries) throw e; await sleep(2 ** (attempt + 1) * 1000); } }}
// Submit a task, resubmitting only once we've confirmed none was createdasync function submitSafely(platform, query) { const started = Math.floor(Date.now() / 1000) - 60; // allow for clock skew for (let attempt = 0; attempt < 4; attempt++) { try { return await post("/task/submit", { platform, query }); } catch (e) { if (e instanceof MaxAEOError && (e.ret === 1024 || e.ret === 1025)) { await sleep(15_000 * (attempt + 1)); // no task was created; wait and try again continue; } if (e instanceof MaxAEOError && e.ret !== -1) throw e; } // Outcome unknown: look for the submission we just made const found = await getWithRetry("/task/key/list", { keyword: Array.from(query.trim()).slice(0, 256).join(""), platform, start_time: started, page_size: 5, }); if (found.items.length > 0) { return getWithRetry("/task/get", { task_id: found.items[0].task_id }); } } throw new Error("Submission failed after several attempts; try again later");}Contacting us about an issue
Section titled “Contacting us about an issue”When you need our help investigating, please include:
- the
task_id(for task-related issues); - the request time (to the minute) and the endpoint you called;
- the request trace ID from the
ipalfish-trace-idresponse header, if present; - the full
retandmsg, orerror_codeanderror_message.
Never send your full API key to anyone, including our staff.
