AI agents

Error Envelopes Across Tool Types: HTTP, Database, APIs

Antonio Evans10 min read

A top-down flat diagram of three sources, an HTTP response, a database driver error line and a vendor SDK error dialog.
Key points

Key points

  1. 1One envelope with eight fixed fields (class, source, code, message, retryable, retry_after, correlation_id, raw) lets every caller make the same retry or replan decision regardless of where the error came from.
  2. 2HTTP status codes map cleanly to the five classes if you treat 400 and 422 as validation, 401 and 403 as authentication, 429 as rate limit, and 502, 503 and 504 as transient.
  3. 3Database drivers need a lookup on vendor codes and SQLSTATEs, because deadlocks and serialization failures are transient while constraint violations are validation and fail identically on every retry.
  4. 4A 200 response can hide a failure, so each adapter has to inspect the body and not just the status line before it fills the envelope.

Why one envelope beats per-source error handling

A system that talks to a payment API, a Postgres database and a vendor SDK ends up with three error vocabularies. The HTTP client raises an exception with a status code, the driver raises an exception with a numeric code and a message string, and the SDK raises its own typed exception that may or may not wrap a readable HTTP response. The caller, usually an orchestration loop or an AI agent, then has to decide: retry, fix the request, refresh credentials, or stop.

The argument for a single envelope is that this is the same decision every time, and it depends on a small number of facts. Is this error about the request (fix it), about identity (get new credentials), about volume (wait), about the world being temporarily unwell (wait and retry), or about the world being permanently in a state that will not change (stop and replan)? Five classes cover those cases.

This matters more in agent systems than in plain services, because the model reading the error will act on whatever you put in the tool result. Practitioners writing about tool failures note that structured error strings returned as tool results, associated with the original call id, are what let the model reason about what went wrong instead of crashing the run in the n8n guide to tool calling errors. A consistent envelope is that structured string, generalized across every source.

The counterargument, that normalization throws away information, is real but solvable: keep the untouched original in a raw field. You lose nothing, and the caller gets a stable contract.

The envelope contract

Eight fields, all required except where noted:

FieldTypePurpose
classenumvalidation, authentication, rate_limit, transient, permanent
sourceenumhttp, database, vendor_sdk, internal
codestringSource-native code, kept as a string so 23505 and "ThrottlingException" both fit
messagestringHuman-readable, safe to show in a log or a tool result
retryablebooleanWhether a retry of the same request could succeed
retry_afternumber or nullSeconds to wait, only meaningful when retryable
correlation_idstringPropagated from your request, so logs across sources join
rawobjectThe original status, headers and body, truncated

Two design rules make the envelope usable. First, retryable is derived by the adapter from class plus the source-specific detail, never set by the caller. Second, retry_after is always seconds, always a number, and only populated when retryable is true, because a caller that sees a value in that field should honor it.

A serializer for a model or an agent loop can drop raw and keep seven fields, which keeps tool results small. That is the same discipline as curating what goes into a model call: every token in the result competes for attention, and irrelevant detail degrades the model's focus as Anthropic describes in its context engineering guidance.

{
  "class": "rate_limit",
  "source": "http",
  "code": "429",
  "message": "Rate limit exceeded for endpoint /v1/charges",
  "retryable": true,
  "retry_after": 12,
  "correlation_id": "req_8f21c0",
  "raw": { "status": 429, "headers": { "retry-after": "12" } }
}

Mapping HTTP responses into the envelope

Status codes are the easiest source because the classes were designed against them. The HTTP specification groups responses into five classes, with 4xx indicating a client error and 5xx a server error per the MDN reference on status codes, and the retry decision usually falls out of that grouping.

The mapping that holds up in practice:

  • 400 and 422 to validation. Keep the distinction in code but not in class: 400 means the request could not be parsed, 422 means it parsed and business rules rejected it as this breakdown of HTTP status codes explains.
  • 401 and 403 to authentication. Retrying with the same credential will never help, so retryable is false. Treat a token refresh as a separate step the caller may take before replaying.
  • 429 to rate limit. Read Retry-After into retry_after. That header can be seconds or an HTTP date, so parse both forms as the Temporal retry recipe shows.
  • 502, 503 and 504 to transient. Upstream unavailability or a gateway timeout; retry with backoff.
  • Every other 4xx to permanent. 404, 405, 409 and 410 all mean the request as constructed will not work. Replanning beats retrying.

Two refinements earn their keep. The X-Should-Retry header, when a server sends it, should override the status code, which is what the Temporal implementation does in its HTTP retry recipe. And 408 should be treated as transient rather than as a client error, because a request timeout usually indicates an overloaded server rather than a malformed request, a distinction the same recipe makes explicit.

Mapping database driver errors into the envelope

Databases are harder because the code that matters is vendor-specific and sometimes buried. The mapping still lands in the same five classes.

ConditionClassRetryableNotes
Unique or foreign key violation (Postgres SQLSTATE 23505, 23503)validationnoThe row state will not change
Not-null or check constraint failurevalidationnoFix the payload
Deadlock victim (SQL Server 1205)transientyesRerun the transaction, ideally immediately
Serialization failure (Postgres 40001)transientyesSame as deadlock: replay
Lock wait timeouttransientyesBack off longer than a deadlock replay
Permission denied on table or schemaauthenticationnoCredential or grant problem
Connection refused or losttransientyesShort backoff, check pool health
Syntax error in querypermanentnoA code defect, not a data problem

SQL Server's documentation is unusually direct about this: error 1205 means a transaction was chosen as a deadlock victim and rolled back, and the documented user action is to execute the queries again, while also noting that deadlocks are usually application-related and may need code changes in the MSSQLSERVER_1205 reference. That is a textbook transient error with a permanent-error smell, and it is exactly why the adapter exists.

A worked example makes the payoff concrete. An order service under load deadlocks roughly once per few thousand writes. With the envelope, the adapter marks 1205 as transient with retryable: true and a retry_after of zero, because the victim's transaction was rolled back cleanly and replay is safe and immediate. The caller retries once and succeeds. Without the envelope, that 1205 arrives as an opaque exception, the request fails, and the user sees an error. Meanwhile a 23505 on the same table is almost always an orchestration mistake, such as a retried insert with a replayed idempotency key, so marking it validation and retryable: false surfaces the bug instead of hiding it behind retries.

Mapping vendor SDK errors into the envelope

Vendor SDKs vary the most. Some raise typed exceptions, some wrap an HTTP response, and some return an error object inside a success body. The adapter's job is to find the most specific signal available and fall back to the generic shape.

A practical order of precedence:

  1. A typed exception class the SDK documents as retryable (throttling, conflict, service unavailable).
  2. An embedded HTTP status on the exception, which maps through the HTTP table above.
  3. A string error code, matched against a small allowlist you maintain.
  4. The HTTP table applied to a bare status, when nothing else is available.
  5. permanent with the raw body preserved, when nothing at all matches.

The failure mode this guards against is the silent one. Analysis of production tool call failures finds that many of the hardest cases return HTTP 200: a write was accepted but never committed, or a connector swallowed an authentication failure and returned an empty result set that the agent treated as valid state as Scalekit's review of tool call failures describes. No status-code mapping catches that, which is why the adapter must inspect the body for error markers before declaring success, and why raw should carry the full response for offline inspection.

A second subtlety: an expired access token and a revoked refresh token can both surface as 401 at the execution layer but require completely different responses, one of which is a retry after refresh and the other of which is escalation to the user per the same Scalekit analysis. If your identity layer can distinguish them, put that distinction in code and let the adapter set retryable accordingly.

Retry policy per class, with numbers

One classification, five policies:

  • validation: no retry. Return to the caller with the field-level detail so the request can be fixed. If a model produced the request, hand it the envelope and let it repair once; a one-shot repair is worth more than a retry loop.
  • authentication: no retry on the same credential. Attempt at most one refresh and replay, then escalate.
  • rate_limit: wait retry_after if present. Otherwise use exponential backoff with a jitter cap, limited to a small number of attempts.
  • transient: exponential backoff with a jitter cap.
  • permanent: no retry. Replan.

The latency math matters. A retry budget of three with 200ms, 400ms and 800ms backoff adds a guaranteed 1.4 seconds to any call that returns a persistent error, and that cost lands on the user for an error the first response already settled as this analysis of agent retry versus replan argues. The same piece makes the complementary point that many tools that look idempotent are not: a payment call that timed out may have succeeded server-side, and the retry creates a duplicate charge. So retryable: true is not sufficient on its own; the call must also be safe to repeat, which is why idempotency keys belong in the retry layer and not in the envelope.

Randomized jitter, rather than plain exponential backoff, is the production standard because unjittered backoff synchronizes clients into a thundering herd per the retry discussion in the n8n tool error guide.

Worked example: one failing order across three sources

An order service handles a request that touches an HTTP carrier API, a Postgres write, and a vendor tax SDK. Three failures in sequence:

  1. Vendor tax SDK returns a throttling error wrapped in an HTTP 429 with Retry-After: 8. The SDK adapter reads the embedded status, sets class: rate_limit, retryable: true, retry_after: 8.
  2. Postgres write deadlocks. The adapter maps 40001 to class: transient, retryable: true, retry_after: 0. The transaction is rolled back, so replay is safe and immediate.
  3. Carrier API returns 422 because the delivery postcode format was rejected. The adapter maps it to class: validation, retryable: false, and puts the offending field name, if present, in message.

The caller's code has one branch on retryable and one on class, and it behaves correctly against all three sources. The log line for each includes the same correlation_id, so a trace across the SDK, the database and the carrier joins cleanly. That is the payoff: one decision tree instead of three, and a trace a person can read.

Retry behavior across a multi-step workflow compounds fast: at 95 percent per-step reliability across 20 steps, end-to-end reliability falls to 35.8 percent per the reliability math in the Scalekit analysis. A single normalized envelope is what makes it possible to see which class of failure is eating that budget, because you can group failures by class and source instead of grepping five different log formats.

If you would rather not maintain the adapter layer for every model and vendor your system touches, Quanta Inference puts text, image, video, audio and embedding models behind one OpenAI-compatible API key at https://quantaintelligence.ai/inference, which removes one class of vendor SDK from the problem. For teams building the orchestration layer around these APIs, a consulting call with an AI expert can pressure-test the classification table against your actual failure logs.

Testing the envelope with fault injection

An adapter is only as good as its test coverage of the mappings, and production errors are rare enough that you need to inject them deliberately. The Agent Error Simulator framework does exactly this, injecting format, logic and tool call faults at configurable points in a multi-step workflow through a transparent proxy, and its authors report a model-size-dependent threshold for logic error recovery, along with universal catastrophic failure when logic faults are injected at the aggregation step per the IIT paper on the Agent Error Simulator.

The lesson for envelope testing is narrower than the paper's scope but immediately useful: inject faults by class, not by source. If you cover all five classes with one representative per source, you cover the decision tree.

TestInjected faultExpected envelope
Malformed JSON bodyHTTP 400validation, not retryable
Business rule rejectionHTTP 422validation, not retryable
Expired tokenHTTP 401authentication, not retryable
Quota exhaustedHTTP 429 with Retry-Afterrate_limit, retryable, retry_after set
Gateway downHTTP 503transient, retryable
Duplicate keyPostgres 23505validation, not retryable
DeadlockSQL Server 1205transient, retryable
Silent no-opHTTP 200, empty bodypermanent or transient per adapter rule

There is a subtlety in what fault injection actually proves. A fault-injection run only validates recovery if the contradicting evidence was visible before the agent relied on the faulted fact; a study of 1,191 faulted executions found that among 44 runs where counterevidence eventually appeared, only 17 received it before first use per SSCBench on evidential validity. For envelope testing this means asserting on the envelope the adapter produced, not only on the final outcome, because a run can succeed by luck while the classification was wrong.

FAQ

FAQ

Why not just pass the original error through?

Because every caller then needs a per-source decision tree, and the trees drift. A single envelope with a fixed set of fields means retry logic, alerting and logging all key off the same values regardless of origin. The raw field preserves the original for debugging, so nothing is lost.

Which HTTP status codes are safe to retry automatically?

429, 408, 502, 503 and 504, plus 409 when the resource is only temporarily locked. 400, 401, 403, 404 and 422 are not safe to retry, because the request or the credential is wrong per this retry-status breakdown. Honor Retry-After when it is present.

Should I retry when a database reports a deadlock?

Yes, once, and immediately. A deadlock victim's transaction has been rolled back, so replaying it is safe and usually succeeds per Microsoft's documentation of error 1205. Do not retry a constraint violation; the row state has not changed and the second attempt fails identically.

How do I handle a vendor SDK that returns 200 with an error body?

Inspect the body before declaring success. Some connectors return 200 with a no-op body or an empty result set after swallowing an authentication failure, which is the hardest failure class to detect as Scalekit describes. Map the body marker to the appropriate class, and keep the full body in raw.

Where should the retry loop live, in the tool or in the agent?

In the tool or orchestration layer for transient and rate-limit errors, so the model never sees a failure it cannot act on. The convention is to implement retries in the tool itself and return only the final envelope to the agent per Redpanda's system prompt guidance. Validation and permanent errors should reach the model so it can repair or replan.

Sources

Sources

  1. LLM Tool Calling Error Handling: Retries and Fallbacks
  2. Effective context engineering for AI agents - Anthropic
  3. HTTP response status codes - MDN Web Docs - Mozilla
  4. Choosing the Right HTTP Status Codes for Your API
  5. Retry policy from HTTP responses | AI Cookbook
  6. MSSQLSERVER_1205 - SQL Server - Microsoft Learn
  7. Tool Call Failures in Production: Debug and Prevent Guide
  8. Replan, Don't Retry: Why Most Agent Errors Aren't Transient
  9. Agent Error Simulator: Fault Injection for Testing Recovery ...
  10. SSCBench: Evaluating the Evidential Validity of Fault- ...
  11. Write Effective System Prompts | Redpanda Agentic Data ...
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles