CharterError, so a host
application can catch the whole surface with one except. The runtime decides
nothing about what happens next: it raises a typed exception and hands the
decision back.
CharterError
Optional[str]
A documentation slug, such as
"auth/oauth-flow", or None. Set on errors
whose fix is a procedure rather than an edit: registering an OAuth app,
choosing scopes, standing up a callback route. Errors whose message already
states the fix carry none, and neither does ToolValidationError, whose
message is written to be handed back to a model that cannot follow a link.Optional[str]
The slug rendered against the documentation site, or
None. Appended to the
message by str(exc), and readable on its own for a host that formats errors
itself. Appending .md to it returns the page as markdown, which is what an
agent debugging a pack should fetch.tests/test_error_docs.py can resolve
every slug the library can print against the pages that ship beside it. A link
inside an exception reaches someone who is already stuck; a dead one is worse
than none.
DeclarationError
Path(), Query() or Body() marker, a Pagination naming
half a style, an Envelope that could never detect a failure, a Format
naming a transform nobody registered.
It names whose mistake this is, not when it surfaced. Most are raised while a
tool is built or a declaration validated. Some cannot be detected until a request
is assembled, and come out of ainvoke instead — an unmarked field, a body that
will not form-encode, a pack whose host was never configured. Either way the fix
is in the declaration and the reader is its author, which is why these carry a
docs link where an error written for a model does not.
Also a ValueError, deliberately. Several are raised inside a pydantic
validator, which converts ValueError and nothing else, and a host that already
catches ValueError around a declaration keeps working unchanged. except CharterError catches them too.
CredentialError
str
What went wrong.
Optional[str]
Whose credentials failed —
"google" — or None when the failure is not
attributable to one provider.Optional[int]
The HTTP status that triggered it, when it came from a response —
401 by
default, or whatever credential_statuses declared. None when the
credentials were missing locally or a refresh failed.bool
Whether the API refused the token itself, so its provider should drop it.
True for a credential status and an envelope’s credential_errors; False
for an envelope’s permission_errors, where the token works and may not do
this, and for a failure raised before any request. Defaults to whether there
is a status_code.bool
Whether the grant itself is gone, so only the user authorizing again brings
it back. True when a token endpoint answered
invalid_grant or a code its
OAuth2Server declares in
dead_grant_errors, for the repeats of that error during the cool-down, and
when a GrantLoader finds no grant stored.
False for everything else, including an access token the API refused, which
the next call replaces by refreshing.str(exc) prefixes the provider in brackets when there is one.
Raised by: a provider with nothing to hand back (EnvTokenProvider on an unset
variable, an unconfigured pack, a SubjectProvider with no subject set); a
token endpoint refusing a refresh or an exchange; a response whose status is in
credential_statuses; an envelope failure whose code is in
credential_errors; and the constructor guards on StaticTokenProvider,
CallbackProvider, OAuth2Client and OAuth2Flow.
The library never re-authenticates on its own. This is the signal to run
whatever flow you own and retry — see
the OAuth flow.
InsufficientScope
WWW-Authenticate: Bearer error="insufficient_scope" challenge on any response,
or whatever the factory’s ScopeRefusal
declares. A CredentialError, so an existing except CredentialError keeps
catching it; token_refused is always False, because a refresh returns the same
grant.
list[str]
The scopes to add so this call runs, narrowest first. What the provider named,
read against the tool’s declaration so that of several it would accept, the
narrowest the tool lists comes first; the tool’s own declaration when the
provider names nothing.
tuple[str, ...]
Every scope the provider said would let the call through, any one of which will
do, in its order. Empty when it named none, which is how to tell that
needed
came from the declaration.Optional[Kind]
The kind of credential the scopes belong to, when not
the pack’s default:
stripe.RESTRICTED_KEY for a restricted key’s permission,
slack.USER for a bot token on a user-only method.status_code is the response’s status, or 403 when the refusal came inside a
200. See Permissions for
what each provider sends.
ToolValidationError
str
Written to be handed straight back to a model as the tool result: it names the
offending fields and how to fix them.
Optional[str]
Which tool’s input failed, when known.
list[dict[str, Any]]
The structured per-field errors.
[] when none were supplied — never None,
so iterating is always safe.ainvoke validating arguments against
llm_schema(). A call that fails here cost no request, no rate-limit budget and
no money.
See handling it for the retry loop
this is designed for, and
the auto-corrections applied before it
raises.
ContextError
str
The tool that was called.
str
The
FromContext field whose value was
missing or did not fit.str
The key the host was expected to pass it under in
context.ainvoke before any request, and before any rule reads the value,
when the host’s context lacks a value a required FromContext field needs or
holds one of the wrong type. The mistake is the application’s, and the message
says so: the adapters hand a CharterError back to the model as the tool’s
result, and a model told only that a field was missing would try to supply it. A
rule that refuses because of the host’s value raises ToolValidationError
instead, since that one is the model’s to act on.
TransformError
str
What failed.
Optional[str]
The field it was applied to, when known. Appended to
str(exc).apply_transform when the name is not
registered (the message lists what is), when the value does not validate as the
transform’s semantic type, or when the transform function itself raises. Also
before the network.
APIError
int
The status the API returned.
200 when a declared
envelope found the failure inside a
success — that really was the status.str
An excerpt of the response body, truncated to 500 characters so it stays safe
to pass into a context window.
Optional[str]
The request URL, when known.
Optional[int]
Seconds the server asked the caller to wait, parsed from
Retry-After —
usual on 429, common on 503. Only the delta-seconds form is parsed; the
HTTP-date form is legal but rare in JSON APIs, and guessing at clock skew is
worse than reporting nothing.Two fallbacks, for APIs that answer the same question elsewhere. When there is
no Retry-After, X-RateLimit-Reset is read — but only when
X-RateLimit-Remaining is 0, so an ordinary response carrying its quota
headers is not reported as a wait. GitHub needs this: it sends no
Retry-After and answers 403 rather than 429. And an
Envelope with a retry_after
resolver fills this in for an API that refuses inside a 200, which is how a
cost-budgeted API like Shopify reports a throttle.str(exc) joins the status, URL, message, retry hint and body excerpt with em
dashes.
Charter never retries, and retry_after is why: it refuses to discard the
server’s own answer to “when?”, and leaves the policy to you.
NetworkError
OAuth2Client makes before it, an
OAuth2Flow code exchange, and a revocation. The
httpx exception is __cause__.
Nothing was learned about the call itself, so the message says that for the
model reading it: the arguments were not the problem and no change to the call
fixes it. It also says whether the request can have taken effect, because that
is what decides whether repeating it is safe:
while getting an access token — so a model is not told the API was down when
it was never contacted.
bool
False when the connection never opened (a connect timeout, a refused
connection, an exhausted pool), so the request cannot have been received.
True otherwise — a read timeout can follow a request the server received
and acted on, which is how a repeated send_mail sends twice.Optional[str]
The host that could not be reached.
Optional[str]
The request URL without its query string, which can carry identifiers.
may_have_taken_effect is what a host needs to decide
whether to. In observability the call’s outcome is
transport_error and its error_type is the httpx class it wraps.
Catching the surface
InsufficientScope is a CredentialError, so it
goes before it, and catching CharterError first would swallow them all.
DeclarationError is not in that block, and the trailing except CharterError
is what catches it: most of them are raised while a tool is built, but the ones
that need a request to be detected arrive here instead. Because it is also a
ValueError, code that already guards a declaration with except ValueError
keeps catching the build-time ones.
In an agent
A tool failing must end the call, never the agent. The OpenAI loop, the LangChain adapter and the MCP server hand aCharterError back to the model as
the tool’s result, since its message is written to be read. Anything else —
a bug in a response handler or a credential provider — is logged with its
traceback on the charter logger, and the model is told the tool failed on its
side, that its arguments were not the cause, and to tell the user instead of
retrying. The exception’s own text is not passed on: it was written for nobody,
and can quote internal state.
A host running its own loop over Tool.ainvoke gets the same text from
unexpected_failure_text:
Related
- Handling a validation error
- Envelopes — the failures that arrive inside a 200
- Limitations — what the runtime declines to decide for you