Errors in the Aetherfy vector database
The Aetherfy error envelope
Every error the Aetherfy vector API authors is returned in one shape:
{
"error": {
"code": "STABLE_CODE",
"message": "human readable description"
}
}code is the stable, machine-readable identifier — branch on it, never on the
message text, which is written for humans and may be reworded. Alongside the two
required fields, an Aetherfy error may carry extras that depend on the site that
raised it:
| Extra field | Appears on |
|---|---|
details | Errors with structured context |
field, max | Validation failures against a numeric cap |
request_id | Correlation identifier for support |
documentation_url | Errors with a canonical explanation |
collection_name | Collection-scoped failures |
existing_regions | A collection that lives in another region |
agents | A collection still referenced by agents |
size_mb, max_size_mb | Response-size failures |
current_etag | Schema compare-and-set conflicts |
retriable | Errors Aetherfy marks as safe to retry |
The Aetherfy error envelope is universal
Every path authors the envelope, including the storage-engine pass-through
routes. Those routes proxy Qdrant, whose native failure body is shaped
{"status": {"error": "..."}, "time": 0.001} — Aetherfy normalises it on the way
out rather than relaying it, so error.code is always present and a generic
handler can rely on it.
Earlier versions of these docs described POST /api/v1/collections/{name}/points/retrieve
as an exception that relayed the engine body verbatim. That is no longer true.
If you wrote a handler tolerating a missing error.code on that route, the
tolerance is now dead code — harmless, but you can drop it.
Error codes in the Aetherfy vector API
Every code Aetherfy can return, with its HTTP status and whether retrying the identical request can succeed.
| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
MISSING_API_KEY | 401 | No Authorization header, or not a Bearer header | no |
INVALID_API_KEY | 401 | The key does not resolve | no |
ACCOUNT_SUSPENDED | 403 | The account is suspended for billing | no |
COLLECTION_OWNERSHIP_MISMATCH | 403 | The collection belongs to another account | no |
NOT_FOUND | 404 | Unsupported endpoint, or missing collection | no |
COLLECTION_NOT_FOUND | 404 | No such collection | no |
SCHEMA_NOT_DEFINED | 404 | The collection has no stored schema | no |
METHOD_NOT_ALLOWED | 405 | The method is not supported on that path | no |
VALIDATION_ERROR | 400 | A request field failed validation | no |
INVALID_POINT_ID | 400 | A point id is not an unsigned integer or UUID | no |
TOO_MANY_POINTS | 400 | More than 10 000 points in one upsert | no |
RESERVED_FIELD | 400 | A payload key begins __aetherfy_ | no |
MALFORMED_JSON | 400 | The body is not valid JSON | no |
SCHEMA_VALIDATION_FAILED | 400 | The payload violates a strictly-enforced schema | no |
COLLECTION_LIMIT_EXCEEDED | 400 | The tier’s collection cap is reached | no |
INVALID_ENFORCEMENT_MODE | 400 | Not off, warn, or strict | no |
INVALID_SAMPLE_SIZE | 400 | Outside 100 to 10 000 | no |
REQUEST_IDLE_TIMEOUT | 408 | 30 s elapsed with no bytes on an upsert | no |
COLLECTION_EXISTS_IN_OTHER_REGION | 409 | The collection already lives elsewhere | no |
COLLECTION_NAME_TAKEN | 409 | The name is in use by a collection with a different configuration — a re-create with the same configuration is idempotent and returns 200 | no |
COLLECTION_IN_USE | 409 | An agent still references the collection | no |
CLEANUP_IN_PROGRESS | 409 | A prior deletion is still finishing — carries retriable: true | yes |
SCHEMA_VERSION_MISMATCH | 412 | If-Match ETag conflict; body carries current_etag | no |
PAYLOAD_TOO_LARGE | 400 / 413 | Two causes, two statuses: 400 when a single point’s payload exceeds 64 KB, 413 when the upsert wire body exceeds the 500 MB ceiling | no |
RESPONSE_TOO_LARGE | 413 | The response would exceed 10 MB | no |
COLLECTION_REGIONS_EMPTY | 422 | No regions resolved for the request | no |
COLLECTION_REGIONS_NOT_IN_SCOPE | 422 | Requested regions are outside the plan’s scope | no |
STARTER_REGION_CONSISTENCY | 422 | Single-region plan; the resource asked for a different region | no |
RATE_LIMIT_EXCEEDED | 429 | The tier’s per-minute request cap is exceeded | yes |
STORAGE_LIMIT_EXCEEDED | 429 | The tier’s storage cap is exceeded — free space first | no |
INTERNAL_ERROR | 500 | Unhandled server-side failure | no |
PROXY_ERROR | 502 | The proxy could not complete the upstream call | yes |
SERVICE_UNAVAILABLE | 503 | The service is temporarily unable to serve | yes |
GATEWAY_TIMEOUT | 504 | The upstream call timed out | yes |
Aetherfy’s fallback error codes
The codes above are authored deliberately: something in Aetherfy decided that exact failure and named it. The codes below are different in kind. Aetherfy proxies a storage engine and authors the error envelope on the way out — when the upstream body carries a usable code, that code is preserved; when it does not, Aetherfy synthesises one from the HTTP status.
Seeing one of these means the upstream failed without naming its own reason,
so the message carries more signal than the code. Treat them as “something
upstream went wrong, here is roughly what kind” rather than as specific
diagnoses.
| Code | HTTP | Meaning | Retryable |
|---|---|---|---|
BAD_REQUEST | 400 | The engine rejected the request and supplied no code. Read the message | no |
UNAUTHORIZED | 401 | An upstream component refused credentials without naming a code | no |
FORBIDDEN | 403 | An upstream component denied the request without naming a code | no |
CONFLICT | 409 | A conflicting state upstream, unnamed. The specific 409s above are more precise | no |
PRECONDITION_FAILED | 412 | A precondition failed upstream, unnamed — typically an ETag mismatch | no |
RATE_LIMITED | 429 | A 429 from upstream carrying no code of its own | yes |
UPSTREAM_ERROR | 500 / 502 / 503 / 504 | The catch-all: an unmapped upstream status with no code | yes |
RATE_LIMITED is not RATE_LIMIT_EXCEEDED
Both exist, both are 429, and they mean different things. Branch on the code, not the status:
| Code | Who produced it | What it tells you |
|---|---|---|
RATE_LIMIT_EXCEEDED | Aetherfy’s own per-minute rate limiter | You exceeded your plan’s request allowance. Back off; the allowance resets on the next minute bucket |
RATE_LIMITED | The envelope normaliser, from an upstream 429 | Something upstream of Aetherfy throttled the request without naming a code. Not necessarily your plan allowance |
A retry loop keyed on status === 429 will treat these identically, which is
usually fine — both want backoff. A dashboard or alert that reports “you hit your
rate limit” should key on RATE_LIMIT_EXCEEDED specifically, or it will
misattribute an upstream throttle to the customer’s plan.
Which Aetherfy errors are worth retrying
Only five Aetherfy codes can succeed on an identical retry. Everything else is a statement about the request or the account, and repeating it verbatim produces the same answer.
| Retryable code | HTTP | How to retry against Aetherfy |
|---|---|---|
CLEANUP_IN_PROGRESS | 409 | Wait for the prior deletion to finish; the body carries retriable: true |
RATE_LIMIT_EXCEEDED | 429 | Back off on your own schedule — Aetherfy sends no Retry-After header |
PROXY_ERROR | 502 | Retry with backoff |
SERVICE_UNAVAILABLE | 503 | Retry with backoff |
GATEWAY_TIMEOUT | 504 | Retry with backoff |
STORAGE_LIMIT_EXCEEDED is a 429 but is not retryable: nothing frees itself
while you wait. Delete data or raise the cap. See limits.
Exception classes in the Aetherfy Python SDK
The Aetherfy Python SDK maps API errors onto typed exceptions. The base class is
AetherfyVectorsException; catch it to catch everything the SDK authors.
| Attribute on the base class | Meaning |
|---|---|
.message | Human-readable description |
.request_id | Correlation identifier |
.status_code | HTTP status |
.details | Structured context, when present |
.error_code | The stable code from the envelope |
| Exception | Extra attributes |
|---|---|
AetherfyVectorsException | base class |
AuthenticationError | — |
RateLimitExceededError | .retry_after |
ServiceUnavailableError | — |
ValidationError | — |
CollectionNotFoundError | — |
PointNotFoundError | — |
RequestTimeoutError | — |
NetworkError | — |
SchemaValidationError | .errors |
SchemaNotFoundError | — |
CollectionInUseError | — |
CollectionInOtherRegionError | .existing_regions, .requesting_region |
QuotaExceededError | .quota_type, .current, .limit |
PartialUpsertError | .saved, .total, .failed |
.retry_after on RateLimitExceededError is an attribute of the class, not a
value Aetherfy supplies: Aetherfy sends no Retry-After header on a 429, so
there is nothing to populate it from. Do not branch on it — back off on your own
schedule.
The helper is_retryable_error(error) lives in aetherfy_vectors.exceptions and
answers the retry question for you.
import os
import time
from aetherfy_vectors import (
AetherfyVectorsClient,
AetherfyVectorsException,
AuthenticationError,
QuotaExceededError,
RateLimitExceededError,
)
from aetherfy_vectors.exceptions import is_retryable_error
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
for attempt in range(3):
try:
results = client.search("articles", query_vector=[0.1, 0.2, 0.3, 0.4], limit=5)
print([hit.id for hit in results])
break
except AuthenticationError as err:
print("bad key, not retrying:", err.message)
break
except QuotaExceededError as err:
print("quota:", err.quota_type, err.current, err.limit)
break
except RateLimitExceededError as err:
print("rate limited, request_id:", err.request_id)
time.sleep(2 ** attempt)
except AetherfyVectorsException as err:
if not is_retryable_error(err):
print("permanent failure:", err.error_code, err.status_code)
break
time.sleep(2 ** attempt)
client.close()Exception classes in the Aetherfy JavaScript SDK
The Aetherfy JavaScript base class is AetherfyVectorsError — note the different
name from Python’s AetherfyVectorsException.
| Member on the base class | Meaning |
|---|---|
.code | The stable code from the envelope |
.requestId | Correlation identifier |
.statusCode | HTTP status |
.details | Structured context, when present |
toJSON() | Serialisable form of the error |
| Exception | Extra members |
|---|---|
AetherfyVectorsError | base class |
AuthenticationError | — |
RateLimitExceededError | .retryAfter |
ServiceUnavailableError | — |
ValidationError | — |
CollectionNotFoundError | — |
PointNotFoundError | — |
RequestTimeoutError | — |
NetworkError | — |
ConflictError | no Python twin |
CollectionInOtherRegionError | — |
QuotaExceededError | — |
CollectionInUseError | — |
SchemaNotFoundError | — |
SchemaValidationError | — |
PartialUpsertError | — |
.retryAfter on the Aetherfy JavaScript RateLimitExceededError is the same
story as Python’s .retry_after: the member exists on the class, but because
Aetherfy sends no Retry-After header there is nothing to populate it from. Do
not branch on it — back off on your own schedule.
Three utilities ship alongside them from the Aetherfy package root:
createErrorFromResponse, isAetherfyVectorsError, and isRetryableError.
import {
AetherfyVectorsClient,
AetherfyVectorsError,
AuthenticationError,
QuotaExceededError,
RateLimitExceededError,
isAetherfyVectorsError,
isRetryableError,
} from 'aetherfy-vectors';
const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], { limit: 5 });
console.log(results.map((hit) => hit.id));
break;
} catch (err) {
if (err instanceof AuthenticationError) {
console.log('bad key, not retrying');
break;
}
if (err instanceof QuotaExceededError) {
console.log('quota exceeded');
break;
}
if (err instanceof RateLimitExceededError) {
console.log('rate limited, requestId:', err.requestId);
await sleep(2 ** attempt * 1000);
continue;
}
if (isAetherfyVectorsError(err)) {
if (!isRetryableError(err)) {
const e = err as AetherfyVectorsError;
console.log('permanent failure:', e.code, e.statusCode);
break;
}
await sleep(2 ** attempt * 1000);
continue;
}
throw err;
}
}
client.dispose();The duplicate AuthenticationError in the Aetherfy JavaScript SDK
The Aetherfy JavaScript package defines two classes named
AuthenticationError, in two modules. To keep the package root unambiguous, the
one declared in src/auth.ts is re-exported under a different name:
| Declared in | Root export name |
|---|---|
src/exceptions.ts | AuthenticationError |
src/auth.ts | AuthError |
So AuthenticationError imported from the Aetherfy package root is the
transport-level error raised when the API rejects a key, and AuthError is the
key-management one. An instanceof check against the wrong class silently never
matches, so import the name that corresponds to the layer you are catching.
Mapping Aetherfy error names across the two SDKs
| Concept | Python | JavaScript |
|---|---|---|
| Base class | AetherfyVectorsException | AetherfyVectorsError |
| Stable code | .error_code | .code |
| Correlation id | .request_id | .requestId |
| HTTP status | .status_code | .statusCode |
Rate-limit attribute (never populated — Aetherfy sends no Retry-After header) | .retry_after | .retryAfter |
| Retryability helper | is_retryable_error (in aetherfy_vectors.exceptions) | isRetryableError (package root) |
| Type guard | — | isAetherfyVectorsError |
| Conflict class | — | ConflictError |
The caps and tier limits behind most 4xx codes are on limits; the routes that raise them are on the REST API reference.