Errors & retries
When a request fails, the SDK throws an error class you can check with instanceof. Before throwing, it retries failures that are likely to be temporary. Every retried POST reuses the same idempotency key, so the gateway never runs or bills it twice.
Check connection errors before APIError
APIConnectionError, APIConnectionTimeoutError and APIUserAbortError are subclasses of APIError with status set to undefined. Check for them before a general instanceof APIError branch, or check err.status !== undefined.
Error classes
All errors the SDK creates extend GlexirError, which extends Error. Each error's name is its class name.
All of these classes are exported from @glexir/sdk, along with the APIErrorBody type.
APIError
APIError is thrown for any non-2xx response, and for error events inside a stream.
statusnumber | undefinedoptional- The HTTP status.
undefinedfor connection errors, aborts, and errors that arrive mid-stream. messagestringoptional- The status followed by the API's message, for example
"400 Unknown model". If the body is not JSON, the raw body text is used. With no body:"<status> status code (no body)". codestring | null | undefinedoptional- The machine-readable
error.code, for example"rate_limit_exceeded". Branch on this, not on the message. typestring | undefinedoptional- The
error.type, for example"invalid_request_error"or"insufficient_quota". paramstring | null | undefinedoptional- The request parameter the error is about, if any.
errorAPIErrorBody | undefinedoptional- The whole
errorobject from the body:message,type,code,paramand any extra fields. headersHeaders | undefinedoptional- The response headers. Undefined for connection errors and aborts.
requestIDstring | null | undefinedoptional- The
x-request-idheader. Include it when you contact support.
APIError.generate(status, body, message, headers) is the static factory the SDK uses to build the right subclass from a response. You don't need to call it, but it is handy in tests to create the same errors the SDK throws.
The gateway returns errors in OpenAI's shape:
Status codes
| Status | Class | Retried |
|---|---|---|
| 400 | BadRequestError | No |
| 401 | AuthenticationError | No |
| 402 | APIError | No |
| 403 | PermissionDeniedError | No |
| 404 | NotFoundError | No |
| 408 | APIError | Yes |
| 409 | ConflictError | Yes |
| 413 | APIError | No |
| 422 | UnprocessableEntityError | No |
| 429 | RateLimitError | Yes |
| 500 and above | InternalServerError | Yes |
| Any other status | APIError | No |
Statuses without their own class, such as 402 (out of credit or over a spend limit) and 413 (request or file too large), throw a plain APIError. Check err.status and err.code for them:
Every status and code the gateway returns is listed in HTTP error codes.
Connection errors, timeouts and aborts
| Class | When | message |
|---|---|---|
APIConnectionError | fetch threw (DNS failure, refused connection, reset) and retries ran out. The original error is on err.cause. | "Connection error." |
APIConnectionTimeoutError | The last attempt hit the timeout. | "Request timed out." |
APIUserAbortError | Your signal fired, before or during a request or while waiting to retry. Never retried. | "Request was aborted." |
Aborting a stream after its response has started behaves differently; see Aborting a stream.
Errors in streams
If the API sends an error event after a stream has started, iteration throws an APIError with status set to undefined. Its code, type, message and param come from the event. These errors are not retried, because the stream already started. Branch on err.code. See Errors during a stream.
Other errors
| Class | Thrown by |
|---|---|
LengthFinishReasonError | responses.parse() when the response is incomplete because of max_output_tokens, and chat.completions.parse() when a choice has finish_reason: "length". |
ContentFilterFinishReasonError | The same parse() methods when output was cut off by the content filter. |
StrictSchemaError | toStrictJsonSchema() when a schema cannot be made strict, for example an optional property that is not nullable, or a keyword such as oneOf. |
GlexirError | Setup and usage mistakes: no API key (from the constructor), toFile() without a filename, iterating a stream twice, or a stream event that is not valid JSON. |
Retries
The SDK retries a request when:
fetchthrows (a network error), or the attempt times out.- The response status is
408,409,429, or500and above.
It does not retry other statuses, an abort from your signal, or an error that arrives inside a stream that has already started.
The server can override the status check with an x-should-retry header: true forces a retry and false prevents one.
Number of retries
The default is 2 retries, so up to 3 attempts. Set maxRetries on the client, or per call:
Each attempt sends x-glexir-retry-count (0, 1, 2, ...). When retries run out, the SDK throws the error from the last attempt.
Backoff
Before a retry, the SDK waits:
- The
retry-after-msheader, in milliseconds, if present. - Otherwise the
retry-afterheader, as seconds or as an HTTP date. - If neither header is present, or the value is negative or longer than 60 seconds, exponential backoff:
min(0.5 × 2^n, 8)seconds, wherenis the number of retries so far, reduced by a random 0 to 25%.
Network errors and timeouts always use exponential backoff.
| Retry | Base delay | With jitter |
|---|---|---|
| 1st | 0.5 s | 0.375 to 0.5 s |
| 2nd | 1 s | 0.75 to 1 s |
| 3rd | 2 s | 1.5 to 2 s |
| 4th | 4 s | 3 to 4 s |
| 5th and later | 8 s | 6 to 8 s |
Aborting your signal during the wait stops it and throws APIUserAbortError.
Idempotency
The SDK sends an Idempotency-Key header on every non-GET request (POST and DELETE). It generates one random UUID per method call and reuses it on every retry of that call. To set your own key, for example one tied to a job ID so a restarted process does not repeat work, pass idempotencyKey:
How the gateway uses the key
The gateway applies idempotency to POST requests. Keys are scoped to your organization and remembered for 24 hours.
- First request with a key. The gateway runs it. If it succeeds, the status and body are stored. If it fails with any error response (
4xxor5xx), the gateway forgets the key, so a retry with the same key runs fresh. For example, after a402you can add credit and retry with the same key. - Same key, same request, finished. The gateway returns the stored status and body without calling the model again or billing you. The response carries
idempotent-replayed: true. - Same key, still running.
409with codeidempotency_request_in_progress. This happens, for example, when an attempt timed out on your side while the gateway was still working on it. The SDK retries409, so a later retry can get the stored result once the original finishes. - Same key, different request.
422with codeidempotency_key_reused(UnprocessableEntityError). Use a new key for a new request. - Same key, original was streamed. Streams cannot be replayed:
409with codeidempotency_stream_replay(ConflictError). The SDK retries it like any409before throwing. - Key longer than 255 characters.
400(BadRequestError).
"Same request" means the same method, path and JSON body. For multipart uploads (files and audio) the gateway compares only the body size, so do not reuse a key for a different file.