Error Handling & Status Codes
此内容尚不支持你的语言。
GraphQL errors
Section titled “GraphQL errors”Wetel’s API is a standard NestJS GraphQL (Apollo Server) service. Errors are not returned in the
data field — they show up in the top-level errors array of the GraphQL response, each with a
message and an extensions object.
{ "errors": [ { "message": "Agent #42 not found", "extensions": { "code": "NOT_FOUND", ... } } ]}Always branch your client’s error handling on errors[].extensions.code (or, failing that, the
exception’s HTTP-equivalent semantics below), not on parsing message text — message strings are
meant for humans and can change without notice.
A note on precision: Wetel’s backend does not run a custom GraphQL exception filter or a
custom formatError mapping — it relies on Apollo Server’s and @nestjs/graphql’s default
exception-to-extensions.code behavior. In practice this means standard NestJS HTTP exceptions
map to predictable codes (unauthorized/forbidden-style exceptions to something like
UNAUTHENTICATED/FORBIDDEN, validation failures to BAD_USER_INPUT, unhandled errors to
INTERNAL_SERVER_ERROR), but because there’s no custom mapping layer to point to as the source of
truth, treat the exact code string as informative rather than a hard contract, and always log the
full extensions object during integration/debugging rather than hardcoding a single expected
value.
The exception vocabulary the API actually throws
Section titled “The exception vocabulary the API actually throws”Surveying the backend’s resolvers and services, errors are raised using a small, consistent set of standard NestJS exception classes — there is no bespoke error-code enum layered on top. In rough order of how often each shows up in the API:
BadRequestException— malformed input, a business-rule validation failure (e.g. a quota check, an invalid state transition). Most common exception in the codebase.ForbiddenException— the caller is authenticated but not allowed to do this. This is also what you get from most resolvers when no tenant is associated with the calling token (e.g. “No tenant assigned”).NotFoundException— the requested resource (agent, session, workflow, custom action, etc.) doesn’t exist, or doesn’t exist for your tenant — Wetel’s multi-tenancy model returns “not found” rather than a distinct “forbidden” for cross-tenant access attempts, so as not to confirm whether a given ID exists at all under another tenant.UnauthorizedException— authentication itself failed (missing/invalid/expired token).InternalServerErrorException— an unexpected failure the API couldn’t map to a more specific case (e.g. a downstream provider call failed unexpectedly during message processing). If you see this repeatedly for the same operation, it’s worth reporting — it usually means an edge case the API doesn’t yet handle explicitly.ConflictException— rare; used for a small number of state-conflict cases (e.g. attempting an operation against a resource in a state that doesn’t support it).
The specific case of a missing platform header
Section titled “The specific case of a missing platform header”A request missing the required x-huat-platform: customer header produces an error that can look
unrelated to authentication at first glance. This is a known, documented gotcha with its own
explanation and fix — see Authentication rather than duplicating it here. If you’re
seeing an unexplained rejection on every request from a new integration, check that header first.
Workflow run failures
Section titled “Workflow run failures”Failures inside a running workflow (a node erroring out mid-execution) are not the same thing as a GraphQL request error — the mutation that started the session can succeed while a later node in the workflow still fails. These surface through the event/subscription layer:
- The
NodeFailedEventsubscription event, if you’re subscribed to it. workflowRun.nodeTrace, which records what happened node-by-node (JWT-authenticated access only today).
Node trace statuses worth branching on
Section titled “Node trace statuses worth branching on”Each nodeTrace entry carries a status. Three of them mean materially different things, and are
worth handling distinctly rather than collapsing into “something failed”:
FAILED— that node threw, and nothing intercepted it, so the whole run stopped there.FAILED_HANDLED— atool/actionnode threw, but the graph has afailure-labeled edge, so the workflow routed down it and the run itself still completed. Not an outage; the author planned for it.BUDGET_EXCEEDED(added 2026-09-21) — the run exhausted its execution budget and the engine refused to dispatch the named node. Nothing went wrong with that node; it never ran. The run is persisted asFAILED, and the entry carriesbudgetLimit("maxNodeExecutions"or"deadline"),budgetNodeExecutionsandbudgetElapsedMsso the cause is readable without parsing the message string.VISIT_LIMIT_REACHED(added 2026-09-22) — the graph came back to a node that had already used up its ownmaxVisitsfor this run, so the engine refused to dispatch it again. LikeBUDGET_EXCEEDED, nothing went wrong with the node. Unlike it, this does not on its own mean the run failed — read the run’sstatus. The entry carriesvisitLimitandvisitCount. Three outcomes are possible: the node had a'loop_exhausted'edge and the run carried on down it (usuallyCOMPLETED); the node declaredmaxVisitsand had no escape edge, so the run isFAILED; or the node never declaredmaxVisits, in which case that branch simply stops and the rest of the graph continues — the long-standing behavior, which until now left no record at all. Full detail: Workflow Overview: Loops and re-entry and Workflows API: Loop re-entry andVISIT_LIMIT_REACHED.
A run’s ceilings are 250 node executions and 10 minutes of wall-clock time; a graph is
additionally capped at 200 nodes / 400 edges when it is saved or published, which surfaces as
an ordinary BadRequestException on the mutation rather than a run-time failure. Full detail:
Workflow Overview: Graph size and run limits
and Workflows API: Run limits and BUDGET_EXCEEDED.
A run that is waiting is not a run that failed
Section titled “A run that is waiting is not a run that failed”A run with status: "AWAITING_INPUT" paused at an
await_reply node and is waiting on the user’s next message.
It is neither complete nor broken, and it will never transition to COMPLETED — the reply is
executed as a separate run whose resumedFromRunId points back at it. Don’t build alerting
that treats a non-COMPLETED run as a failure without excluding this status; use
pendingAwaitReplies to see what is waiting
and until when.
Full details, event shapes, and query examples are already covered in Workflow API Reference and Troubleshooting — this page won’t duplicate them.
Practical guidance
Section titled “Practical guidance”- Always check
errors[].extensions.codefirst; fall back tomessageonly for logging/debugging. - Don’t assume a specific exception class always maps 1:1 to a specific business meaning across
every operation —
ForbiddenException, for instance, is reused for both “no tenant on this token” and genuine authorization failures. Read themessagefor the specific reason during development. - For a headless / no-UI integration (a bot, a backend
service), build retry/backoff logic around
InternalServerErrorException-class failures — not aroundBadRequestException/ForbiddenException/NotFoundException, which represent a request that will not succeed no matter how many times you retry it unchanged. - Rate-limited operations return their own error when a limit is exceeded — see Rate Limiting for exactly which operations are throttled today.