Skip to main content

Error reference

The Ferro Labs AI Gateway returns errors in the same JSON shape as the OpenAI API, so existing OpenAI SDK error handling works unchanged. Because the gateway is a proxy in front of 30 upstream providers, every error has a source: it either originates in the gateway itself (bad request, auth, budget, plugin rejection) or is passed through from the upstream provider. The table below labels each row so you can tell the two apart โ€” the column that pure proxy users need most.

Error envelopeโ€‹

Every gateway-generated error is a single JSON object with one error field. The helper that writes it (internal/apierror) emits exactly three string fields:

{
"error": {
"message": "the gateway is at capacity for this request; retry shortly",
"type": "rate_limit_error",
"code": "provider_saturated"
}
}
FieldDescription
messageHuman-readable explanation of what went wrong.
typeBroad error category (invalid_request_error, authentication_error, permission_error, not_found_error, rate_limit_error, insufficient_quota, upstream_error, server_error).
codeStable, machine-readable identifier you can branch on (e.g. model_not_found, insufficient_quota, provider_saturated).
note

Errors the gateway generates โ€” including a pass-through connection failure, now reported as 502 upstream_error in this same envelope โ€” carry Content-Type: application/json. Two things sit genuinely outside it: a response the pass-through proxy forwards verbatim keeps the upstream provider's own status code and body (whatever shape that provider uses), and a path this instance doesn't route at all falls through to the embedded dashboard, answering a plain-text 404 page not found unless the request is a browser asking for HTML. See the notes under Status codes.

Status codesโ€‹

The Source column tells you where the error was decided:

  • Gateway โ€” the gateway rejected, could not route, or refused the request itself; either no provider was contacted, or the gateway re-shaped a provider's status into its own error taxonomy.
  • Provider โ€” the failure is the upstream provider's: forwarded verbatim by the pass-through proxy, or reported through the gateway's envelope after it re-attributed a provider status/failure to the caller.
Retry-After

Retry-After is set in exactly two cases, and it can win on a status other than 429: whenever the underlying error carries an upstream wait hint (Retry-After or X-RateLimit-Reset from the provider's own response) it is forwarded whatever the final status ends up being โ€” a 502 upstream_error built from a throttled 503 still keeps that wait. Otherwise, if the final status is 429, the gateway adds a constant Retry-After: 1. No other status gets the header โ€” 402 insufficient_quota in particular never does, so a client's first refusal is its last request rather than a retry loop against an answer that cannot change.

HTTP Statustype / codeMeaningSourceWhat to do
400invalid_request_error / invalid_requestMalformed JSON body, or a required field failed validation (e.g. missing model).GatewayFix the request body.
400invalid_request_error / request_rejectedA before_request plugin (word filter, max-token, a custom guardrail) rejected the request.GatewayAdjust the request to satisfy the guardrail, or change the plugin config. message carries the plugin's own reason.
400invalid_request_error / unsupported_parameterThe request used an OpenAI parameter the resolved provider can't express, and compatibility.on_unsupported_param is set to reject.GatewayDrop the named parameter, or route the model to a provider that supports it โ€” check GET /v1/capabilities.
400invalid_request_error / provider_not_resolvedPass-through /v1/* request named no provider and no target owns any model in the body.GatewaySet the X-Provider header (e.g. X-Provider: openai) or include a top-level model field in the body.
400invalid_request_error / invalid_proxy_pathThe pass-through path contains a ..-style traversal segment (checked before any provider credential is attached).GatewayRemove the traversal segment from the request path.
400invalid_request_error / streaming_not_supportedPOST /v1/completions (the legacy endpoint) was called with stream: true โ€” that endpoint never streams, regardless of provider.GatewayUse /v1/chat/completions for streaming, or drop stream on /v1/completions.
401authentication_error / missing_api_keyMissing or non-Bearer Authorization header on a protected route.GatewaySend Authorization: Bearer <key>.
401authentication_error / invalid_api_keyThe bearer token is unknown or revoked.GatewayIssue or rotate a valid key.
401authentication_error / authentication_requiredA scope-checked route had no authenticated key in context.GatewayAuthenticate before calling the endpoint.
402insufficient_quota / insufficient_quotaThe budget plugin's per-key USD cap is exhausted.GatewayStop retrying โ€” this status is deliberately outside every OpenAI SDK's retry set and carries no Retry-After. Wait for cost roll-off, or raise/reset the key's budget.
403permission_error / insufficient_scopeAuthenticated key lacks the required scope (e.g. read_only calling an admin write, or a non-admin key hitting /debug/*).GatewayUse a key with the admin scope, or request the needed scope.
404invalid_request_error / model_not_foundNo configured target serves the requested model. Two distinct causes share this exact status/type/code: no target names a provider for it at all (routing never called anyone), or the provider that owns it has since retired the model upstream (routing called the right target and it said no).Gateway or ProviderUse a model a configured target serves โ€” check GET /v1/models. This also covers a target that can't stream, embed, or generate images for the model; there is no separate status for a capability miss.
404invalid_request_error / provider_not_foundPass-through /v1/* request set X-Provider to a name no configured target serves.GatewayUse a provider name from GET /v1/models, or omit X-Provider and let the body's model resolve it.
404not_found_error / resource_not_foundAn admin resource named by id โ€” an API key, a config history version โ€” doesn't exist.GatewayCheck the id; list the resource first (GET /admin/keys, GET /admin/config/history).
404(plain text) 404 page not foundThe path matches no route this instance serves (not /v1/*, /admin/*, a probe, or a dashboard asset), and the request either isn't GET/HEAD or didn't ask for text/html.GatewayCheck the path and HTTP method โ€” this is Go's default 404, not the JSON envelope. The same unmatched path answers a browser GET with the dashboard's 200 app shell instead.
405invalid_request_error / method_not_supportedWrong HTTP method on a route the gateway itself handles.GatewayUse a method from the response's Allow header (every GET route also accepts HEAD).
429rate_limit_error / rate_limit_exceededThe per-IP token bucket is exhausted, a rate-limit plugin rejected the request, or the resolved provider itself returned 429 on a routed surface (its status and code are kept as-is).Gateway or ProviderBack off and retry โ€” Retry-After is always set (see the tip above).
429rate_limit_error / provider_saturatedThe target's concurrency.max_concurrency + queue_size are both full; this request would exceed both.GatewayBack off and retry (Retry-After: 1), or raise targets[].concurrency, or add another target.
500server_error / plugin_errorA plugin's Execute returned an error โ€” it broke rather than denying the request with Reject. Guardrail, auth, ratelimit, and transform plugins fail closed on this; logging/metrics plugins fail open and never reach the client.GatewayRetry; if persistent, check the plugin's own logs/dependency (e.g. a rate-limit plugin's backend store being unreachable).
500server_error / internal_errorInternal configuration fault โ€” e.g. an unparseable provider base URL discovered while building a pass-through request (/v1/*, /v1/files*, /v1/batches*).GatewayFix the provider's <PROVIDER>_BASE_URL configuration.
500server_error / routing_errorAny routing or plugin failure not covered by a more specific status above. Rare โ€” most failure modes now have their own classification.GatewayRetry; if persistent, inspect gateway logs.
501invalid_request_error / proxy_not_supportedThe resolved provider can't serve this as an OpenAI-compatible pass-through โ€” a native-wire-only provider (Anthropic, Gemini, Bedrock, Cohere, Vertex AI, Azure) or one with no proxy support at all.GatewayUse the provider's natively-handled endpoint (chat, embeddings, images) instead of an unhandled /v1/* path.
501invalid_request_error / batch_not_configuredbatch_target is unset, or names a target whose provider isn't registered or isn't batch-capable.GatewaySet batch_target to a configured target on a batch-capable provider (openai, azure-openai, groq, novita, qwen).
501invalid_request_error / responses_not_configuredresponses_target is unset, or its provider isn't registered โ€” applies only to the stateful /v1/responses/{id} sub-routes (retrieve/cancel/delete/input_items). POST /v1/responses (create) still routes by model and is unaffected.GatewaySet responses_target to a configured target.
502upstream_error / response_rejectedAn after_request plugin rejected the upstream response.GatewayLoosen the guardrail, or fix what the provider returned.
502upstream_error / upstream_auth_errorThe resolved provider rejected the gateway's own credential (upstream 401/403).ProviderThis is an operator-side credential problem, not the caller's โ€” rotate the provider API key in the gateway's environment.
502upstream_error / upstream_errorEither the provider returned an unclassified 5xx on a routed surface, or the gateway couldn't reach or read the provider at all while proxying /v1/*, /v1/files*, /v1/batches*, or /v1/responses*.ProviderTransient โ€” retry with backoff. If persistent, check the provider's status page and the gateway's outbound network egress.
503upstream_error / upstream_unavailableThe target's circuit breaker is open, so the request was refused without an upstream call this time.GatewayRetry shortly. Check gateway_circuit_breaker_state{provider="<target>"} and GET /admin/logs?provider=<target> for what tripped it.
503(forwarded verbatim)The provider returned 503 Service Unavailable on the raw, unhandled /v1/* pass-through; its status and body are forwarded exactly as received.ProviderRetry with backoff; the provider is overloaded or in maintenance.
504upstream_error / upstream_timeoutThe resolved provider's own connection timed out.ProviderTransient โ€” retry with backoff.
504upstream_error / gateway_timeoutThe gateway's own request_timeout elapsed, or the caller's own context deadline was exceeded, before any provider produced a usable response.GatewayRaise request_timeout if the model genuinely needs longer, or check whether the target is slow or unhealthy.
Pass-through is transparent, connection failures are not

On the raw /v1/* pass-through (any endpoint the gateway doesn't handle natively โ€” /v1/fine_tuning, /v1/realtime, and similar) a response the upstream actually sent โ€” any 2xxโ€“5xx, 429 and 503 included โ€” is forwarded with the provider's own status code and body, credentials redacted. A failure to reach or read the upstream at all (DNS, TLS, connection refused, a truncated body) is different: nothing came back to forward, so the gateway reports it itself as 502 upstream_error in the standard JSON envelope described above.

Streaming errorsโ€‹

Once a streaming response has started, the HTTP status line is already 200 OK and headers are flushed, so failures cannot be reported with a new status code. Instead the gateway writes a final mid-stream data: event carrying the same error envelope, then closes the connection. There is no trailing data: [DONE] after an error event.

Event codetypeTriggerSource
stream_errorstream_errorThe upstream provider emitted an error chunk mid-stream.Provider
stream_timeouttimeout_errorNo chunk arrived on the gateway's internal channel within the idle window (2 minutes by default).Gateway

Example terminal error event:

data: {"error":{"message":"stream timed out waiting for next chunk","type":"timeout_error","code":"stream_timeout"}}

Client code should treat any streamed event whose top-level key is error as a terminal failure for that request. For the full wire format, the [DONE] sentinel, and idle-timeout details, see the Streaming (SSE) contract.