When something goes wrong
Look up an error
#Paste the error
Read errors in one order: HTTP status, then error.type, then error.code. The status is always meaningful; type tells you who rejected the call; code is stable only for errors the gateway raised itself.
#How to branch
- 1
Switch on the status
402 means top up, 403 means key scope, 429 means wait, 5xx means retry. This layer never lies and never changes.
- 2
Check error.type
xiaoxiang_error is ours. Anything else — openai_error, claude_error, gemini_error — is the vendor’s own body, forwarded unchanged.
- 3
Then read error.code
Only branch on code for xiaoxiang_error. Upstream codes belong to the vendor and can change without notice.
Not every failure has a JSON body
Error envelopes
Five shapes reach your client. Handle all five — the last three are the ones integrations usually miss.
Gateway error
The gateway itself rejected the call — auth, quota, routing, request shape.
{
"error": {
"message": "insufficient account balance, please top up to continue (request id: 20260910...)",
"type": "xiaoxiang_error",
"code": "insufficient_user_quota"
}
}Upstream error, passed through
The vendor answered with its own error. The body is forwarded unchanged, so type and code are the vendor’s.
{
"error": {
"message": "This model's maximum context length is 128000 tokens.",
"type": "invalid_request_error",
"code": "context_length_exceeded"
}
}Guard rejection
Idempotency and a few other pre-relay guards answer before the relay envelope exists. Flat shape, uppercase code.
{
"code": "IDEMPOTENCY_IN_PROGRESS",
"error": "a request with this Idempotency-Key is still processing"
}Async task rejection
Every task endpoint — video, image task, music, 3D — reports failures in its own flat shape. There is no nested error object here, so a parser written against /v1/chat/completions reads null.
{
"code": "task_not_exist",
"message": "task does not exist",
"data": null
}Status only, empty body
Rate-limit rejections and oversized request bodies abort with a bare status.
HTTP/1.1 429 Too Many Requests
Retry-After: 20
Content-Length: 0Response headers
Log the request id on every response, not just failures. It is the identifier support traces a call by, and it is the difference between a five-minute answer and a day of guessing.
| Header | Sent when | What to do with it |
|---|---|---|
| X-Oneapi-Request-Id | Every response, success or error. | Log it beside your own trace id. Support answers tickets from this value, and the same id is appended to gateway error messages. |
| Retry-After | On 429, and on 409 while an Idempotency-Key is still processing. | Wait at least this many seconds. It is the only backoff hint the gateway sends. |
| X-Idempotent-Replayed | A stored response is replayed for a repeated Idempotency-Key. | true means no new work ran and nothing was charged again — treat the body as the original result. |
| X-Idempotency-Sealed | The original response cannot be replayed safely (for example a stream or an oversized body). | Stop retrying that key. Reconcile from the task endpoint or your own records instead. |
const response = await fetch(url, init);
const requestId = response.headers.get("X-Oneapi-Request-Id");
if (!response.ok) {
// Bodies are absent on 429 and 413 — never assume JSON here.
const detail = await response.text();
logger.error({ status: response.status, requestId, detail });
throw new GatewayError(response.status, requestId, detail);
}#Error-code table
Errors the gateway raises itself, always with type "xiaoxiang_error". Two rows share status 429 and mean opposite things — read the code before you back off.
| HTTP | Code | Meaning | What to do | Retry? |
|---|---|---|---|---|
| 401 | none | Missing, malformed, deleted or disabled bearer token. This layer sets no code — match on the status. | Check the Authorization header and the key state in Console → Keys. | no |
| 401 | signature_required | The key has request signing on and the request lacks X-Timestamp, X-Nonce or X-Signature. | Sign every request made with this key (Authentication & keys → Request signing), or turn signing off on the key. | no |
| 401 | signature_expired | X-Timestamp is not 10-digit unix seconds or is more than 5 minutes from server time. The response carries X-Server-Timestamp. | Correct the client clock by the X-Server-Timestamp difference and sign again with a new nonce. | yes |
| 401 | signature_invalid | X-Signature does not match the key’s signing secret, or X-Nonce is malformed. | Sign timestamp, nonce, method and path (no query string) joined by newlines with the current secret from Console → Keys. | no |
| 401 | signature_replayed | This X-Nonce was already used with this key in the last 10 minutes. | Generate a new random nonce for every request, including retries. | yes |
| 403 | access_denied | The caller IP is outside the key’s IP allowlist. | Add the egress address in Console → Keys, or drop the allowlist for keys with no fixed IP. | no |
| 403 | none | Account suspended, or the key is not allowed to use this model or channel. | Check the key’s model whitelist first; if it is unrestricted, open a ticket — the account itself is blocked. | no |
| 400 | unsupported_endpoint | The model exists but does not serve this path. The message lists the endpoints it does serve. | Move the call to a listed endpoint — image task models do not answer on /v1/chat/completions. | no |
| 400 | read_request_body_failed | The request body could not be read — truncated upload, wrong Content-Type, or a stream that ended early. | Send a complete body with a matching Content-Type. Resending the same broken request fails the same way. | no |
| 402 | insufficient_user_quota | Wallet balance cannot cover the amount pre-reserved for this call. | Top up in Console → Wallet, then retry. | no |
| 429 | insufficient_user_quota | The account’s daily spend cap is reached. Same status as throttling, opposite meaning — backoff will not clear it. | Raise or clear the cap in account settings, or resume tomorrow. Branch on the code, not on 429 alone. | no |
| 429 | none | Per-key RPM/RPD throttle or a concurrency guard. The body is empty; Retry-After carries the wait. | Sleep for Retry-After seconds, then retry with exponential backoff and jitter. | yes |
| 413 | read_request_body_failed | Request body exceeds the accepted size. The transport-level limit aborts with an empty body. | Shrink the payload — upload files through /v1/media/upload-policy and send the URL instead of Base64. | no |
| 422 | invalid_request | No channel supports this media intent for the model — for example image-to-video on a text-to-video-only route. | Check the model card for supported intents and switch model or intent. | no |
| 503 | model_not_found | Unknown model name, or no healthy channel currently serves it. Note the status: an unknown model is 503, not 404. | Verify the name against /v1/models, then retry — failover usually finds a healthy channel on the next attempt. | yes |
| 500 | sensitive_words_detected | The prompt hit the content filter before it left the gateway. | Change the prompt. Retrying the same text always fails. | no |
| 500 | count_token_failed | The gateway could not estimate tokens for pricing, usually from a malformed message array. | Validate the messages array; if it is well-formed, retry and open a ticket with the request id. | yes |
| 500 | invalid_request | The request could not be parsed into a shape this endpoint accepts. Note the status — a body the gateway cannot read is reported as 500, not 400. | Validate the JSON against the endpoint reference before retrying. | no |
| 502 | do_request_failed | The vendor was unreachable, or returned a failure the gateway could not recover through failover. | Retry with backoff — the next attempt usually lands on a different channel. | yes |
| 504 | do_request_failed | The vendor did not answer within the gateway deadline. | Retry with backoff; long prompts and busy vendors time out more often. | yes |
| 499 | do_request_failed | Your client closed the connection before the answer arrived — usually your own HTTP timeout firing mid-stream. | Raise the client read timeout. Long generations need minutes, not the default of most HTTP libraries. | no |
Idempotency guard errors
These come from the replay guard in front of the relay, so they use the flat {code, error} shape with an uppercase code instead of the OpenAI envelope.
| HTTP | Code | Meaning | What to do | Retry? |
|---|---|---|---|---|
| 400 | IDEMPOTENCY_STREAM_UNSUPPORTED | Idempotency-Key was sent on a streaming request. | Drop the header for streaming calls, or set stream:false when you need replay protection. | no |
| 400 | IDEMPOTENCY_BODY_UNREADABLE | The body could not be hashed for the idempotency ledger. | Send a normal JSON or multipart body — chunked bodies that cannot be buffered are rejected. | no |
| 409 | IDEMPOTENCY_PAYLOAD_CONFLICT | This key was already used with a different body, path or query. | Generate a new key. A key identifies one exact request, not one retry loop. | no |
| 409 | IDEMPOTENCY_IN_PROGRESS | The first request with this key is still running. | Wait for Retry-After (2s) and retry the same key — you will get the original response. | yes |
| 409 | IDEMPOTENCY_RESULT_UNREPLAYABLE | The original request finished but its response was sealed and cannot be replayed. | Do not resubmit blindly. Reconcile through the task endpoint or your own records first. | no |
| 503 | IDEMPOTENCY_UNAVAILABLE | The idempotency ledger is unavailable, so the guard fails closed rather than risk a double charge. | Retry with backoff — the request was not executed. | yes |
Async task errors
Video, image-task, music and 3D endpoints answer with their own flat shape — {code, message, data} — for submit, poll and cancel alike. One quirk to plan for: polling an unknown task id is 400, cancelling one is 404.
| HTTP | Code | Meaning | What to do | Retry? |
|---|---|---|---|---|
| 400 | task_not_exist | Polling or remixing a task id that does not exist, or belongs to another account. Note the status — polling an unknown task is 400, while cancelling one is 404. | Persist task_id at submit time and poll with exactly that value. | no |
| 400 | invalid_request | A required field is missing — prompt on a remix, task_id on a fetch. | Fix the body or the path before retrying. | no |
| 400 | task_channel_disable | A follow-up task (remix, extend) is pinned to the original task’s channel, and that channel is now disabled. | Submit a fresh task instead of deriving from the old one. | no |
| 400 | invalid_source_task | The source task is the wrong kind for this operation — for example a video id passed to a 3D continuation. | Check that the source id came from the same capability family. | no |
| 404 | task_not_exist | Cancelling a task id that does not exist or is not yours. | Nothing to cancel — treat it as already gone. | no |
| 409 | task_not_cancellable | The task already reached a terminal state, so there is nothing to cancel. | Poll once more and use the result you already paid for. | no |
| 501 | task_cancel_not_supported | The provider behind this task exposes no cancellation API. Most do not — cancellation is the exception, not the rule. | Let the task finish. Design the UI so cancelling is best-effort, not a guarantee. | no |
| 502 | cancel_task_failed | The provider was unreachable while cancelling. The task is unchanged. | Retry once, then fall back to letting it finish. | yes |
Still stuck?
Send us the request ID or the error message and we will look into it.