Error Handling
All errors from DVARA use a consistent JSON envelope regardless of which surface returned them. Three separate services emit errors, and the tables below are grouped by source so that every row maps to behavior that actually runs:
- Data plane errors — the DVARA LLM Gateway on port
8080, serving/v1/*and the actuator surface. The Enterprise build also exposes/actuator/license; the Open Source distribution does not. - Flightdeck errors — DVARA Flightdeck on port
8090, serving the Flightdeck. - MCP Gateway errors — served by the gateway on port
8080alongside/v1/*(the separate MCP process was retired in 1.7.0). Approval-gate, loop-detection, and other agentic errors share the same envelope shape as the rest of the MCP errors below.
A single error code lives in exactly one of these three tables — there's no global "500-row catalog". If you can't find a code below, it means it isn't reachable on the surface you're looking at.
Error Envelope
{
"error": {
"message": "Human-readable description of the error",
"type": "invalid_request_error | provider_error | gateway_error",
"code": "machine-readable error code",
"param": "field name, if validation error",
"trace_id": "the request's X-Trace-ID, or a 32-character hex ID"
}
}
| Field | Type | Description |
|---|---|---|
message | string | Human-readable error description |
type | string | Error category (see table below) |
code | string | Machine-readable error code |
param | string | Field name that caused the error (validation only) |
trace_id | string | Trace ID for debugging: the request's X-Trace-ID if it sent a valid one (1–128 characters from A-Z a-z 0-9 . _ : -), otherwise a 32-character hex ID |
Data plane errors
Emitted by the DVARA LLM Gateway on port 8080 for every /v1/* request.
From 1.8.5, /v1/messages and /v1/messages/count_tokens give the same
status and code in Anthropic's envelope instead:
{"type":"error","error":{"type":...,"message":...,"code":...},"request_id":...}.
There error.type is Anthropic's type for the status, such as
authentication_error for 401 or permission_error for 403. See
Use the Anthropic Messages API.
| Status | type | code | When |
|---|---|---|---|
| 400 | invalid_request_error | validation_error | Missing required field on the request DTO (model, messages). Emitted by request-body validation, before the gateway's exception path. |
| 400 | invalid_request_error | no_provider | No provider on the route matches the requested model, or the matching provider has no credential configured. |
| 400 | invalid_request_error | no_capable_provider | The route has providers but none of them support the requested response_format, tool calls, or tool calls on a stream; or no registered provider supports the Batch API. |
| 400 | invalid_request_error | invalid_request | Generic request-shape rejection (missing response_format.type, etc.) after the body has parsed successfully. |
| 400 | invalid_request_error | provider_rejected_request | The provider refused the request itself (upstream 400, 404, 413 or 422). The message carries the provider's reason. Not retried, and no failover: the same request fails the same way anywhere. |
| 400 | invalid_request_error | invalid_json | Malformed JSON body — parsing failed before request-DTO binding. Emitted when the JSON body is malformed or unparseable. |
| 400 | invalid_request_error | unsupported_response_format | Defensive provider-level check for response_format types the provider can't handle. In normal flow, capability-aware routing intercepts these requests first and returns no_capable_provider; this code surfaces only when routing bypasses capability filtering (for example, explicit model-prefix routing). |
| 400 | invalid_request_error | unsupported_capability | The selected provider does not support the content in the request. Raised by Gemini, Bedrock and Ollama for an https image URL when image fetching is off. Before 1.10.0 Cohere and Groq also raised it for images and for tool-use / tool-result content blocks, and Mistral for images; from 1.10.0 those are sent to the provider, and a model that cannot take them answers with the provider's own error. Also raised for other unsupported request features, such as seed on Anthropic or Bedrock, input_audio or file content parts, or a tool type other than function and custom. Capability-aware routing does not pre-filter vision content on the primary route, so the provider itself rejects the request. Tool calls are pre-filtered; see no_capable_provider. |
| 400 | invalid_request_error | prompt_experiment_not_running | The named prompt experiment is not running, has no variants, or its variant weights do not total 100. Correct the experiment before retrying. |
| 400 | invalid_request_error | unsupported_api_version | From 1.8.5, on /v1/messages: the anthropic-version header is missing or is not 2023-06-01. From 1.12.0 2026-10-21 is served too, and what that version removed (temperature, top_p, top_k, a document in an assistant message, citations on a user's text) is refused with invalid_request. |
| 400 | context_window_error | context_window_exceeded | Estimated input tokens reach the workspace's hard threshold, 90 % of the model's context window by default (guardrail.context.hard-threshold-pct), and no pruning strategy is configured. The window is the model's model_limits entry in gateway.yaml, then the Enterprise model catalogue, else the provider default. In 1.8.5 the check used one limit per provider; see the known issue. |
| 400 | pii_violation | pii_detected | PII detected in the request and the workspace policy is BLOCK. |
| 403 | pii_violation | pii_tokenize_unavailable | TOKENIZE was selected but this build has no reversible token store. The provider was not called. |
| 403 | pii_violation | pii_redact_unavailable | A legacy v1 code retained for compatibility when Enterprise tokenization cannot obtain usable key or store capacity. Only TOKENIZE reaches it; the response includes attempted_action: "TOKENIZE". |
| 402 | budget_exceeded | budget_cap_hard | Hard budget cap reached on the workspace or the API key. |
| 402 | budget_exceeded | per_call_cost_exceeded | A single request's estimated cost (max_tokens × the output price, or the reasoning price when that is higher, plus the input tokens × the input price) exceeded the per-call ceiling, set by dvara.llm-gateway.finops.per-call-max-cost-usd or Workspace.metadata["cost.per-call-max-usd"]. Complements the accumulated-spend cap; off by default. |
| 403 | policy_violation | policy_denied | Request blocked by an active policy rule. |
| 403 | guardrail_violation | guardrail_blocked | Guardrail detector flagged the request at or above the configured risk threshold and the workspace action is BLOCK. |
| 403 | data_residency_error | data_residency_violation | Geo-aware routing found no provider in the workspace's allowed regions, or, from 1.10.0, the provider failed and no fallback in them was left. |
| 403 | ip_access_error | ip_access_denied | Client IP blocked by global or per-workspace CIDR rule. |
| 403 | workspace_credential_required | workspace_credential_required | Strict-BYOK enforcement rejected the call — the workspace has no own active provider credential. Set under dvara.credentials.require-workspace-credential=true (global) or Workspace.metadata["credentials.require-workspace-credential"] (per-workspace). Audit event: PROVIDER_CREDENTIAL_MISSING. |
| 403 | workspace_suspended | workspace_suspended | Workspace administratively suspended — trial-cliff expiry, chronic-abuse sweep, self-delete schedule, or manual operator action. Audit event per blocked request: WORKSPACE_SUSPENDED_BLOCK. When the workspace record holds a reason for the suspension, the error carries it as its own field, "reason" (for example "trial_expired"), so a client can tell one kind from another without reading the message; the field is absent when no reason was recorded. From 1.8.1 a suspension applies to the next request, and lifting one is served just as quickly; before that, either direction could take about a minute on the LLM plane. |
| 403 | session_killed | session_killed | From 1.8.5, Enterprise platform. The request's X-Session-Id names a killed session. The message is "Agent session has been killed: <id>. Start a new session." It is 403, not 429, so clients do not retry. A kill lasts 24 hours. GET /v1/models and calls with no session id are not checked. See Stop a session. |
| 413 | input_size_error | input_too_large | Request exceeds guardrail.max-input-tokens, guardrail.max-messages-per-request, or guardrail.max-message-length. |
| 422 | schema_validation_error | schema_validation_failed | The response failed output-schema validation. DVARA does not retry this failure automatically. |
| 429 | rate_limit_error | rate_limit_exceeded | Per-key request count or token budget exhausted in the current 60-second window. Emitted directly by the rate-limit filter. |
| 429 | priority_throttle_error | priority_throttled | Priority admission controller rejected the request because the workspace's tier is over its concurrency threshold. |
| 429 | provider_rate_limited | provider_rate_limited | The provider itself answered 429, or the upstream provider credential's published rate-limit budget is exhausted and the gateway shed the request after finding no capable non-shed fallback. A shed carries a Retry-After header derived from the upstream signal and requires dvara.llm-gateway.provider-rate-limit.enabled=true. |
| 502 | provider_error | provider_response_too_large | From 1.8.5. The provider's response passed dvara.llm-gateway.response-limits.max-body-bytes (64 MiB) or, for a stream or file download, max-stream-bytes (128 MiB). Refused before it is parsed; not retried, and not counted against the provider's circuit breaker. |
| 502 | provider_error | provider_error | Upstream provider returned a 5xx, 401 or 403 (or another error status not listed above), the connection failed, or a stream ended without a finish reason, had an empty body, or carried an in-band error. Provider-specific detail is prefixed to the message. An upstream 400/404/413/422 is provider_rejected_request, and an upstream 429 is provider_rate_limited. |
| 503 | provider_unavailable | provider_circuit_open | The provider's circuit breaker is OPEN; no upstream call was attempted. |
| 503 | workspace_credential_unavailable | workspace_credential_unavailable | A workspace credential exists but cannot be decrypted or resolved, or its store cannot be read. DVARA does not fall back and emits PROVIDER_CREDENTIAL_UNAVAILABLE. Enterprise platform. |
| 503 | provider_unavailable | failover_capability_mismatch | Primary provider failed and no capable fallback exists. Response also carries the X-Gateway-Failover-Blocked: capability_mismatch header. |
Streaming can fail after the HTTP 200 and SSE headers have been sent. In that
case the status cannot change. A final content-filter event ends delivery and
the audit trail records STREAM_TOO_LARGE_TO_SCAN,
STREAM_TOOL_ARGUMENTS_UNENFORCEABLE, or
STREAM_INCOMPLETE_UNGROUNDED. Read STREAMING_ENFORCEMENT_SUMMARY for the
separate scan_incomplete, scan_failed, and truncated facts. Do not retry
blindly: reduce an over-limit response, fix malformed tool arguments, or retry a
truncated upstream only when the operation is safe to repeat.
Prompt-template codes (data plane)
Emitted when a request references a template via metadata.prompt_template_id
and the resolver cannot satisfy the reference.
| Status | type | code | When |
|---|---|---|---|
| 400 | invalid_request_error | prompt_template_not_active | Referenced template is not in ACTIVE status. |
| 400 | invalid_request_error | prompt_variable_missing | Required {{variable}} not provided in metadata.prompt_variables. |
| 404 | not_found_error | prompt_template_not_found | The template id is unknown, or it belongs to another workspace. |
DVARA Open Source resolves active templates from gateway.yaml, but it does not
run prompt experiments. If a request supplies metadata.prompt_experiment_id,
the Open Source data plane returns HTTP 400 with
code: unsupported_capability; it does not ignore the experiment and send the
call as ordinary traffic. Prompt experiments require the Enterprise platform.
Additional guardrail error codes
| Status | type | code | When |
|---|---|---|---|
| 403 | guardrail_violation | hallucination_detected | The grounding-detection layer fires with action: BLOCK — response contains ungrounded claims. |
| 500 | guardrail_plugin_error | guardrail_plugin_error | An external guardrail plugin configured with fail-mode: CLOSED failed to complete its call or returned a malformed 200, or the semantic guard with fail-mode: CLOSED could not evaluate (from 1.10.0). |
Flightdeck errors
Flightdeck, on port 8090, is a web application. It has no error envelope and no list of error codes for a caller to branch on. When it refuses a change, it says why in words on the page: a message on the form, which keeps what you typed, or a notice at the top of the page.
The *_not_found, invalid_* and *_duplicate codes that used to be listed here belonged to the Admin API, which was removed in 1.7.0. Two /v1/admin paths remain on Flightdeck: POST /v1/admin/config/import, for a GitOps import from a script, and GET /v1/admin/config/bundle/signing-key. Any other /v1/admin/** path returns 404.
A new A2A agent ID may not contain ::, which DVARA uses to join an agent to a skill. The Flightdeck form and a GitOps import both refuse such an ID and name it. Use -, _ or . instead. An agent that already has such an ID keeps it.
Authentication and authorization
Requests to /v1/*, /internal/* and /actuator/* get these JSON bodies. The codes are upper case. Every other Flightdeck path answers a refused request with a plain 401 or 403 error response, whatever the caller.
| Status | Body | When |
|---|---|---|
| 401 | {"error":{"code":"UNAUTHORIZED","message":"Authentication required","type":"authentication_error"}} | Missing or invalid bearer token, PAT or JWT. |
| 403 | {"error":{"code":"FORBIDDEN","message":"Insufficient permissions","type":"authorization_error","method":"…","path":"…"}} | The caller lacks the role the path needs. Audited as AUTHORIZATION_DENIED with the method, path and principal. |
MCP Gateway errors
Emitted by the MCP plane, which runs inside the gateway on port 8080 under /mcp. Almost every error — auth, policy, agentic governance, upstream failure, workspace suspension — comes back through the same lowercase-code, type: "mcp_error" envelope.
{
"error": {
"message": "…",
"type": "mcp_error",
"code": "mcp_server_not_found",
"trace_id": "…"
}
}
From 1.8.6, native POST /mcp limits the complete request body with
dvara.mcp-gateway.limits.max-request-bytes, 4 MiB by default. A larger
declared or chunked body receives a bare HTTP 413 before JSON-RPC processing,
so it has no DVARA error envelope or code. Authentication runs first; an
invalid credential still receives 401 without its body being read. This
outer limit does not cover the server-scoped MCP REST bridge.
| Status | code | When |
|---|---|---|
| 400 | mcp_not_available | The MCP implementation needed for this operation is not available in this build. Do not use this code to infer licence state. |
| 400 | mcp_upstream_rejected | From 1.8.4. A native MCP server answered the call with a JSON-RPC invalid request, method not found or invalid params error, such as an unknown tool. Fix the call; a retry will fail the same way. error.upstream_error carries the server's code, message and data. |
| 410 | mcp_transport_sunset | From 1.8.5. The MCP server uses the legacy HTTP+SSE transport and its sunset date has passed (2027-07-28 by default, set by dvara.mcp-gateway.legacy-sse-sunset). Move the server to Streamable HTTP. |
| 413 | mcp_arguments_too_large | From 1.8.5. The tool call's arguments, as JSON, are larger than dvara.mcp-gateway.limits.max-arguments-bytes (1 MiB by default) or the workspace's own limit. Refused before the schema check, policy, the PII scan and the server. |
| 400 | mcp_request_state_invalid | From 1.8.5. The client's requestState for an input round trip is not a string or is larger than 16 KiB. The same code comes back as 502 when the server sent such a state. |
| 400 | mcp_pii_detected | PII detected in the tool-call arguments or response, workspace policy is BLOCK. |
| 401 | mcp_auth_required | Missing Authorization: Bearer <key> on the MCP Gateway request. |
| 401 | mcp_auth_invalid | API key does not resolve to an active workspace. |
| 401 | mcp_auth_revoked | API key has been revoked. |
| 403 | mcp_policy_denied | MCP request denied by a policy rule. |
| 403 | mcp_approval_denied | A human reviewer denied the tool call from the DVARA Flightdeck approval queue. |
| 403 | mcp_approval_unavailable | From 1.8.4. The tool call needed approval, the approval store could not be reached, and the workspace's default approval action is deny. No reviewer could act. Before 1.8.4 this was a 502, which clients retry. |
| 403 | mcp_workspace_suspended | The calling workspace is suspended. Since 1.7.0 each plane checks workspace state before forwarding, so suspension stops LLM requests, MCP tool calls, and A2A hops. The body is {"error":{"message":"Workspace is suspended","type":"mcp_error","code":"mcp_workspace_suspended","trace_id":"…"}}. |
| 403 | mcp_session_killed | The session has been killed; later tool calls for that session are refused at once. From 1.8.5 a kill made on any plane, or by loop detection, counts, and it lasts 24 hours. A call held for approval when the kill lands is refused too. |
| 403 | mcp_sampling_refused | From 1.8.5. The server answered with a request for the client to run a model call (sampling/createMessage). DVARA never relays it, because that model call would skip the LLM Gateway's governance. |
| 403 | mcp_input_request_refused | From 1.8.5. The server asked for input the workspace does not allow, such as a roots request (refused by default) or an elicitation the workspace turned off. |
| 404 | not_found (type not_found_error) | Wrong URL under /mcp (e.g. typo, excluded actuator path, bot probe). Measured on 1.7.0 — a path no controller claims is answered with the neutral envelope, not the mcp_error one. Returned when the route or path is unknown so probes to non-existent paths don't masquerade as 500 mcp_internal_error. |
| 404 | mcp_server_not_found | MCP server ID not registered for the calling workspace. |
| 408 | mcp_approval_timeout | Approval gate reached its configured timeout with no human decision; workspace default action is deny. |
| 429 | mcp_rate_limit_exceeded | Per-key rate limit exhausted. |
| 429 | mcp_agent_loop_detected | The loop detector fired (repetition threshold, cycle detection, or rate cap) on the session. |
| 503 | mcp_injection_scan_failed | A tool response could not be scanned and the workspace injection action is BLOCK or SANITIZE; FLAG records the failure and returns the response. |
| 502 | mcp_response_too_large | From 1.8.5. The tool result passed dvara.mcp-gateway.response-limits.max-body-bytes (4 MiB) or, for a stream, max-stream-bytes (16 MiB), or the workspace's own limit. Refused before it is parsed and not retried. |
| 502 | mcp_upstream_error | From 1.8.4. A native MCP server answered the call with any other JSON-RPC error. error.upstream_error carries the server's code, message and data. |
| 502 | mcp_server_unavailable | A native MCP server could not be reached: its address did not resolve, it refused the connection, it did not answer in time, or its session could not start. The message names which. From 1.9.0 a STDIO server whose process exits during a call fails that call at once with this code, on every protocol version, instead of after a 30-second wait; the next call starts the server again. From 1.8.4 a server that answers the call with a JSON-RPC error is no longer reported this way; see the two codes above. |
| 502 | (fall-through) | Any other gateway-thrown error code; the MCP Gateway lowercases it, prefixes mcp_ if needed, and returns 502 mcp_error / mcp_<code>. |
| 503 | mcp_server_unavailable | The MCP server is registered but not active, for example suspended, disabled or quarantined; the message names the state. |
| 500 | mcp_internal_error | Any unhandled exception — caught by the generic handler branch. |
From 1.8.4 an upstream JSON-RPC error comes back as the server gave it. A call
to an unknown tool on POST /mcp/{serverId}/tools/call returns:
{
"error": {
"message": "The MCP server answered with an error: Unknown tool: search_artcles",
"type": "mcp_error",
"code": "mcp_upstream_rejected",
"upstream_error": {
"code": -32602,
"message": "Unknown tool: search_artcles"
},
"trace_id": "a6783439db1f46a6bfed511a0011e955"
}
}
upstream_error.data is present when the server sent data. The pooled
session to the server is kept.
The native JSON-RPC /mcp endpoint represents governance refusals with
application-specific JSON-RPC codes rather than this HTTP envelope. See the
native MCP refusal table.
Error Examples
Validation Error — Missing Model
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $DVARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Hi"}]}'
{
"error": {
"message": "model is required",
"type": "invalid_request_error",
"code": "validation_error",
"param": "model",
"trace_id": "a6783439db1f46a6bfed511a0011e955"
}
}
No Provider Configured
{
"error": {
"message": "No provider configured for model: gpt-4o. Set OPENAI_API_KEY to register the provider.",
"type": "invalid_request_error",
"code": "no_provider",
"trace_id": "d354d2faaa5f4e14939aa8c480fb9d90"
}
}
Unsupported Response Format
{
"error": {
"message": "Ollama provider does not support response_format json_schema. Supported formats: [text, json_object]",
"type": "invalid_request_error",
"code": "unsupported_response_format"
}
}
Credential Not Found
{
"error": {
"message": "Required credential not found: provider.openai.api-key (platform default)",
"type": "not_found_error",
"code": "credential_not_found",
"trace_id": "f1234567890abcdef1234567890abcde"
}
}
Rate Limit Exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-Trace-ID: a6783439db1f46a6bfed511a0011e955
{
"error": {
"message": "Rate limit exceeded",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"trace_id": "a6783439db1f46a6bfed511a0011e955"
}
}
Provider Error
Error messages include a human-readable hint for common HTTP status codes:
{
"error": {
"message": "Anthropic API error 401 (invalid API key - check your provider credentials)",
"type": "provider_error",
"code": "provider_error",
"trace_id": "b7894561cd2e4f38a1cc820d1f1f5044"
}
}
Hints are appended for these status codes:
| Status | Hint |
|---|---|
| 401 | invalid API key - check your provider credentials |
| 403 | access denied - your API key may lack required permissions |
| 404 | model or endpoint not found - check the model name |
| 429 | rate limited by provider - too many requests |
| 500 | provider internal server error |
| 502 | provider returned bad gateway |
| 503 | provider is temporarily unavailable |
Circuit Breaker Open
When a provider fails repeatedly, the gateway stops sending requests to it temporarily to avoid cascading failures. The error message explains what happened and what to do:
{
"error": {
"message": "Provider anthropic is temporarily unavailable — too many recent requests failed, so new requests are paused. This usually resolves automatically after a short cooldown. If it persists, check your provider API key and account status.",
"type": "provider_unavailable",
"code": "provider_circuit_open",
"trace_id": "c8905672de3f5049b2dd931e20206155"
}
}
PII Detected (Block Mode)
{
"error": {
"message": "Request blocked: PII detected (EMAIL, SSN)",
"type": "pii_violation",
"code": "pii_detected",
"trace_id": "00127894ab5c6d71e4ff153f42428377"
}
}
Guardrail Blocked
{
"error": {
"message": "Request blocked: guardrail violation detected (INJECTION, JAILBREAK)",
"type": "guardrail_violation",
"code": "guardrail_blocked",
"trace_id": "11238905bc6d7e82f500264053539488"
}
}
Input Too Large
{
"error": {
"message": "Request exceeds maximum messages limit: 150 > 100",
"type": "input_size_error",
"code": "input_too_large",
"trace_id": "22349016cd7e8f930611375164640599"
}
}
Schema Validation Failed
{
"error": {
"message": "Output schema validation failed: $.name: required property missing",
"type": "schema_validation_error",
"code": "schema_validation_failed",
"trace_id": "33450127de8f90041722486275751600"
}
}
Context Window Exceeded
{
"error": {
"message": "Request exceeds context window: 150000 tokens estimated, max 128000",
"type": "context_window_error",
"code": "context_window_exceeded",
"trace_id": "44561238ef9001152833597386862711"
}
}
Failover Blocked
HTTP/1.1 503 Service Unavailable
X-Gateway-Failover-Blocked: capability_mismatch
{
"error": {
"code": "failover_capability_mismatch",
"message": "Failover blocked: no fallback provider supports response_format: json_schema. Primary provider [openai] failed: upstream down"
}
}
Licence limits
An Enterprise install without a licence may make 100,000 governed calls a month: LLM calls, MCP tool calls and A2A hops counted together. Once the month's count reaches 110,000, each plane refuses the call with 402 and its own code. On /v1/*:
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": {
"code": "license_token_cap_exceeded",
"type": "token_cap_exceeded",
"message": "Without a licence this install can make 100,000 calls a month, counting LLM calls, MCP tool calls and A2A hops together. This month: 110,000. Calls resume on the 1st, or when the platform owner adds a licence in Flightdeck.",
"tier": "unlicensed",
"current_usage": 110000,
"cap": 100000,
"upgrade_url": "https://dvarahq.com/pricing"
}
}
/mcp/* answers 402 with code: mcp_cap_exceeded, and /a2a/* with code: a2a_cap_exceeded and type: a2a_error, with the same message. A refused call is not counted. Calls resume on the 1st of the month, or on the next request after the platform owner adds a licence in DVARA Flightdeck. No restart is needed.
A valid licence has no call limit. From 1.8.6 it counts calls without refusing any, for the monthly usage report. An expired licence counts calls but never refuses one. Until 1.8.2, an expired licence refused /mcp/* and /a2a/* with 402 LICENSE_EXPIRED; that response no longer exists. Check where the install stands with Enterprise GET /actuator/license and DVARA_ACTUATOR_API_KEY. See Get and apply a license.
A2A Gateway errors
Not included in DVARA Open Source.
Where the code is, from 1.9.0. On a JSON-RPC call to /a2a/*, error.code
is a number, as the A2A specification requires: the specification's own where it
names the error, and -32000 for a refusal by DVARA. The code in this table is
in error.data.code with A2A 0.3 (or no version), and in
error.data[0].metadata.code with A2A 1.0, where error.data[0] is a
google.rpc.ErrorInfo whose domain is dvarahq.com for a refusal by DVARA and
a2a-protocol.org for an error the specification defines. Before 1.9.0 the code
was the text of error.code. A stream's error event has the same form. The
Agent Card request keeps a plain HTTP error (a google.rpc.Status for an A2A 1.0
client). See Read a
refusal.
The status, from 1.9.0. A refusal of a JSON-RPC call (a POST to
/a2a/*) is answered with HTTP 200, as the official A2A SDK's server answers
it, so an SDK client can read it. A missing, expired or revoked key is the
exception: it keeps 401. The status in the table below is the refusal's
own: DVARA sends it only when the operator sets
a2a.protocol.json-rpc-error-status: refusal, and it is what the hop history
shows. An Agent Card request, a request that is not a POST, and a 415 for a
body that is not JSON keep their status. See The HTTP status of a
refusal.
| Status | Code | Meaning |
|---|---|---|
| 400 | a2a_not_available | The A2A implementation is unavailable in this build. |
| 400 | a2a_pii_detected | The configured PII action refused the message. |
| 413 | a2a_message_too_large | An outbound A2A message carries more text than a2a.limits.max-message-length (50,000 characters by default). Checked before policy and the PII scan on message:send and message:stream. |
| 400 | a2a_version_not_supported | From 1.9.0. The request named an A2A-Version DVARA does not serve; it serves 0.3 and 1.0 (0.3 only if the operator turned 1.0 off). The body is the A2A VersionNotSupportedError: JSON-RPC code -32009, with this code in error.data.code and the served versions in error.data.supportedVersions (a version that does not start 0. gets them in the google.rpc.ErrorInfo metadata, the versions as 0.3,1.0). Send 0.3, 1.0, or no version. See Name the A2A version. |
| 400 | a2a_parse_error | From 1.9.0. The request body is not JSON (JSON-RPC code -32700). Before, this was a2a_invalid_request. |
| 400 | a2a_invalid_request | The request body is not a JSON-RPC request, or a request to POST /a2a/{agentId} names no method (JSON-RPC code -32600). |
| 404 | a2a_method_not_found | From 1.9.0. The JSON-RPC method sent to POST /a2a/{agentId} is not an A2A method in the version of the request, for example SendMessage (an A2A 1.0 name) in a 0.3 request. JSON-RPC code -32601; the message names the right method. See Name the method. |
| 400 | a2a_unsupported_operation | From 1.9.0. A method DVARA knows but does not serve on the JSON-RPC address, such as GetExtendedAgentCard: read the Agent Card at GET /a2a/{agentId}/.well-known/agent-card.json instead. JSON-RPC code -32004; the message gives the reason. |
| 400 | a2a_invalid_params | From 1.9.0. The request is not valid A2A in its version: a message part that is not exactly one known kind, a member the version does not define, or a value such as "role": "user" in a 1.0 request. JSON-RPC code -32602; the message names the place and the reason, for example params.message.parts[1]: unknown member image. Nothing is sent to the peer. See Send A2A 1.0 messages. |
| 400 | a2a_untranslatable | From 1.9.0. Something in the request cannot be carried in the A2A version DVARA uses for the peer, such as a filename on a text part for a 0.3 peer, or ListTasks to a 0.3 peer. DVARA refuses rather than drop part of the message. |
| 502 | a2a_upstream_invalid_response | From 1.9.0. The peer answered in a shape DVARA cannot read in the peer's A2A version. The answer is not passed on; on a stream, one error event ends the stream. |
| 502 | a2a_upstream_untranslatable | From 1.9.0. The peer's answer cannot be carried in your A2A version, such as a second page of push configs for a 0.3 client. |
| 401 | a2a_auth_required, a2a_auth_invalid, a2a_auth_revoked | The plane key is missing, invalid, or revoked. |
| 401 | a2a_delegated_token_required | A delegated agent call lacks its delegated token. |
| 402 | a2a_cap_exceeded | The monthly call limit is used up: the install-wide limit of an install without a licence (see Licence limits), or a hosted plan's A2A band. |
| 403 | a2a_policy_denied, a2a_scope_denied, a2a_workspace_suspended | Policy, scope, or workspace posture refused the call. |
| 403 | a2a_session_killed | The session is on the workspace deny list. From 1.8.5 a kill made on any plane counts and lasts 24 hours. A running stream ends with an a2a_session_killed error event, and a hop held for approval is refused. |
| 403 | a2a_delegation_chain_required, a2a_delegation_chain_invalid | The delegation guard refused a hop with no chain from an agent's key, or a chain that was changed, expired, or belongs to another workspace, session or agent. |
| 403 | a2a_delegation_scope_widened | From 1.9.0. A signed delegation chain sent in X-Dvara-Delegated-Authorization: DvaraDeleg … has a step that grants more than the step before it: an agent or skill, or an audience, its parent never had. The hop is not forwarded. reason is scope-widened. |
| 403 | a2a_delegation_expired | From 1.9.0. A step of the signed delegation chain is outside its time window, or its window is longer than the gateway allows (a2a.delegation.max-token-lifetime-seconds, 15 minutes by default; a workspace can set a shorter limit). Ask the caller for a fresh chain. reason is expired. |
| 403 | a2a_delegation_invalid | From 1.9.0. The signed delegation chain did not verify, or does not allow this hop. reason says which check failed: malformed, unsupported, unresolved-key (no trusted key for a signer), bad-sig, broken-chain, wrong-audience (the chain may not be shown to this agent), agent-binding (the chain was given to another agent than the API key acts as), or not-granted (it does not grant this skill on this agent). Also wrong-workspace: a version 2 chain names another workspace than the one the call is in; and revoked: a step of the chain, or the agent that signed it, was withdrawn in Flightdeck. unverified: the workspace requires a signed chain, and the header held a plain bearer token. version-not-allowed: the workspace accepts only chains tied to it (version 2), and the chain is version 1. |
| 403 | a2a_approval_denied, a2a_approval_unavailable | The required human approval was denied or unavailable. |
| 404 | a2a_agent_not_found | The agent is absent or outside the caller's workspace. |
| 415 | a2a_unsupported_media_type | From 1.9.0. The request body was not application/json or application/a2a+json. The Accept header of the answer names the two types. See Choose the content type. |
| 408 | a2a_approval_timeout | The approval window expired. |
| 409 | a2a_delegation_cycle, a2a_delegation_depth_exceeded | The delegation guard refused a hop that loops back or makes the chain too deep. Do not retry. |
| 409 | a2a_delegation_hop_budget_exceeded | From 1.8.5. The session has forwarded as many hops as a2a.delegation.max-hops-per-session allows. Do not retry. |
| 429 | a2a_loop_detected | Loop detection stopped the agent chain. |
| 429 | a2a_session_rate_limited | From 1.8.5. The session passed the hop rate (60 a minute). Unlike a loop, the session is not killed. |
| 502 | a2a_peer_version_not_supported | From 1.9.0. The peer agent answered that it does not support the A2A version DVARA sent it. The message names the peer. On a stream it arrives as one error event. Check which A2A versions the peer serves. |
| 502 | a2a_peer_protocol_unsupported | From 1.9.0. The peer's Agent Card offers no way to call it that DVARA can use: no JSON-RPC entry at an A2A version DVARA serves (today 0.3). Nothing is sent to the peer. The message lists what the peer offers. Ask the peer to list a JSON-RPC 0.3 interface. See How DVARA talks to each peer. |
| 502 | a2a_peer_url_refused | From 1.9.0. The address on the peer's Agent Card failed a check that the registered endpoint passes: it is on another host, port or scheme, it is not a plain web address, or the card's signature does not allow it. Nothing is sent to the peer, not even at the registered endpoint. The message names the check. Register the address the card lists, or ask the peer to list the registered endpoint. See Which address DVARA calls. |
| 502 | a2a_upstream_content_type | From 1.9.0. The peer agent answered with a content type DVARA cannot read, such as an HTML page, where it expected JSON or an event stream. The message names the type. On a stream it arrives as one error event. Check the peer's endpoint URL and that it is an A2A agent. |
| 502 | a2a_endpoint_not_https | From 1.8.5. The agent's endpoint is not https://. DVARA does not contact the peer. Move the agent to https:// or disable it. |
| 502 | a2a_response_too_large | From 1.8.5. The peer's answer passed a2a.response-limits.max-body-bytes (16 MiB) or, for a stream, max-stream-bytes (64 MiB). Not retried; a stream is cut and ends with an error event. |
| 502 | a2a_card_unavailable, a2a_card_signature_invalid | Agent Card retrieval or signature verification failed. From 1.8.4 this includes a peer's authenticated extended Agent Card that its public card declares but that could not be fetched; DVARA does not fall back to the public card. |
| 503 | a2a_agent_unavailable | The registered agent cannot serve traffic. |
| 503 | a2a_delegation_binding_unavailable | Since 1.8.4: the gateway could not read which agent the API key acts as, in a workspace that requires a chain from agents, or (from 1.9.0) for a signed delegation chain, with reason binding-unavailable. A retry may pass. |
| 503 | a2a_delegation_settings_unavailable | From 1.8.5. The workspace's delegation settings could not be read and this gateway has never read them. The hop is not forwarded or counted. A retry may pass. |
| 503 | a2a_delegation_hop_budget_unavailable | From 1.8.5, under the hardened preset only. The session's hop count could not be read in time. A retry may pass. |
| 503 | a2a_delegation_hop_budget_below_quorum | From 1.9.0, under the hardened preset only. The gateway's cluster is smaller than the minimum size the operator set for the session hop count. A retry may pass once the gateways are back. |
| 403 | a2a_delegation_root_key_onward | From 1.9.0. A root key (one that may start chains) sent a hop that carries a delegation chain, and the workspace keeps root keys to starting work. Use the agent's ordinary key. |
| 503 | a2a_push_not_available | From 1.8.5. A push config was sent, but the gateway has no A2A_PUSH_CALLBACK_BASE_URL, and the workspace has not chosen direct delivery. |
The reason of a refused delegation chain, from 1.9.0. The three
a2a_delegation_* codes for a signed chain name a group; the check that failed is
next to the code, so a client can act on it without reading the message: in
error.data.reason with A2A 0.3, and in error.data[0].metadata.reason with A2A
1.0. It is there on message:send, on a stream's error event and on task calls.
For example, at 1.0:
{"jsonrpc": "2.0", "id": "1", "error": {"code": -32000,
"message": "Delegation chain refused (bad-sig): a block's signature does not verify",
"data": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "A2A_DELEGATION_INVALID", "domain": "dvarahq.com",
"metadata": {"code": "a2a_delegation_invalid", "traceId": "…", "reason": "bad-sig"}}]}}
Flightdeck shows the same on the A2A hop pages: a refused chain with its reason, and on the workspace's A2A audit page a verified chain with its depth and how it narrowed from step to step.
From 1.8.5 a peer that posts a push notification to the gateway gets 413 when
the notification is larger than a2a.push.max-body-bytes, and 502 when the
gateway could not deliver it to the workspace's webhook. The peer can send it
again.
An unmapped A2A code returns HTTP 502. DVARA preserves an existing a2a_ prefix or adds it before returning the lowercased code.
Additional canonical codes
| Status | Code | Distribution | Meaning |
|---|---|---|---|
| 400 | invalid_path | Both | The data-plane path is not valid. |
| 401 | api_key_required, invalid_api_key, api_key_revoked, api_key_expired | Both | The data-plane credential is missing or unusable. |
| 402 | license_token_cap_exceeded | Enterprise | The monthly call limit is used up on /v1/*. See Licence limits. |
| 402 | mcp_cap_exceeded, a2a_cap_exceeded | Enterprise | The monthly call limit is used up on /mcp/* or /a2a/*. See Licence limits. |
| 403 | api_key_scope | Both | A valid key lacks the scope for this operation. |
| 403 | grounding_unavailable | Enterprise | Grounding was required but its evidence or engine was unavailable. |
| 403 | mcp_scope_denied, a2a_scope_denied | Enterprise | A plane key lacks the required scope. |
| 404 | batch_not_found | Both | The batch is absent from this workspace. |
| 404 | file_not_found | Both | The file is absent or belongs to another workspace; both cases deliberately look the same. |
| 503 | workspace_credential_unavailable | Enterprise | A stored workspace credential exists but cannot be used; DVARA does not fall back. |
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
400 no_provider | API key not set for the provider | Set the environment variable (e.g., OPENAI_API_KEY) |
403 data_residency_violation | Data residency policy violation | Configure routing to use providers in allowed regions |
400 unsupported_response_format | Provider doesn't support the format | Use a different provider or remove response_format |
400 no_capable_provider | No provider on the route supports it | Add a capable provider to the route |
400 pii_detected | PII found and action is BLOCK | Remove the value, use irreversible REDACT, or use LOG while calibrating |
403 pii_tokenize_unavailable | This build has no reversible token store | Use REDACT, or run the Enterprise tokenization prerequisites |
403 pii_redact_unavailable | Enterprise TOKENIZE cannot mint a token | Restore its token store or key path; inspect attempted_action, then retry |
403 guardrail_blocked | Injection/jailbreak/content detected | Review prompt content; change workspace guardrail action to FLAG or LOG |
413 input_too_large | Request exceeds input size limits | Reduce message count, message length, or total input tokens; increase workspace limits via metadata |
422 schema_validation_failed | Response doesn't match JSON schema | Review the registered schema via the Flightdeck and tighten or relax it. DVARA validates once and does not retry. |
400 context_window_exceeded | Input at or over 90 % of the window, no pruning | Reduce conversation length; configure pruning strategy via workspace metadata (guardrail.context.pruning-strategy) |
402 budget_cap_hard | Hard budget limit exceeded | Wait for the next budget period or raise the cap in Flightdeck |
400 compliance_not_available | Compliance reporting is not available in this build | Run an Enterprise build that includes reporting |
400 mcp_not_available | The requested MCP implementation is not available in this build | Run a build that includes that MCP capability |
401 mcp_auth_required | Missing Bearer token on MCP Gateway | Include Authorization: Bearer gw_<key> header |
401 mcp_auth_invalid | Invalid API key on MCP Gateway | Verify the API key is correct |
401 mcp_auth_revoked | API key revoked on MCP Gateway | Generate a new API key |
404 mcp_server_not_found | MCP server ID does not exist | Verify server ID via the Flightdeck |
403 mcp_policy_denied | MCP request denied by policy | Check policy rules via the Flightdeck. Review mcp_server, mcp_tool, mcp_arg conditions. In 1.12.0 an mcp_arg condition never sees the tool's arguments on live traffic, so a rule using it matches every call to that tool. |
400 mcp_upstream_rejected | The MCP server refused the call, e.g. unknown tool or bad arguments | Read error.upstream_error.message; fix the tool name or arguments. Do not retry unchanged. |
502 mcp_upstream_error | The MCP server answered with a JSON-RPC error | Read error.upstream_error and the server's own logs for the trace_id |
502 mcp_server_unavailable | MCP server unreachable, timed out, or its session did not start | Check the server's address and health; the call can be retried |
403 mcp_approval_unavailable | Approval store unreachable, default action deny | Restore Flightdeck and its approval store; retrying before then fails the same way |
503 mcp_server_unavailable | MCP server suspended/disabled | Re-activate the server or check its upstream health in Flightdeck |
429 rate_limit_exceeded | Too many requests | Wait for Retry-After seconds, or increase limits |
502 provider_error | Upstream provider returned an error | Read the hint in the error message (e.g., "invalid API key"). For 401: check your provider API key env var. For 5xx: check the provider's status page. The gateway retries automatically. A provider 429 arrives as 429 provider_rate_limited instead: reduce the request rate. |
400 provider_rejected_request | The provider refused the request (unknown model, bad field, too large) | Read the provider's reason in the message and fix the request. Retrying unchanged fails the same way. |
503 provider_circuit_open | Too many recent provider failures | The gateway pauses requests to a failing provider to prevent cascading errors. It resolves automatically after a short cooldown. If it persists after restarting: check your provider API key and account status. |
503 failover_capability_mismatch | Primary failed, no capable fallback | Add more providers to the route that support the capability |