/ Docs / v0.3 / Concepts

Request flow

What the gateway does with each call, and what the agent gets back when it says no.

/ DocsRequest flow

Every call goes through the same steps. Any step can stop it.

  1. Authenticate. Verify the session token. The token names the provider, so the agent can't pick another.
  2. Match. Turn the call into an action and its params. Unknown paths, params, fields and arguments are rejected here. So are paths with ambiguous encodings (//, %2f, %2e...) and method-override headers (X-HTTP-Method-Override and friends), which could make the upstream see a different call than the gateway checked.
  3. Decode. For actions with a body decoder (e.g. Gmail messages.send), read the body into r.context. A body that can't be decoded is denied.
  4. Decide. Run the policy, then any checks.
  5. Forward. Add the real credential and send the exact bytes that were checked to the provider's fixed upstream. Only a short list of request headers is passed on (accept, content-type, if-match...); cookies and the session token are dropped. The response comes back as-is, minus cookies and connection headers.

Errors

Every error message starts with Error for waterlock proxy: , followed by the reason. The agent can tell the gateway refused the call, not the provider, and won't treat it as an outage to retry.

Errors are handled per provider. Each provider answers in the shape its official SDK parses, so a denial surfaces like any other error from that SDK, with its usual error class, code and message. The shape, and what the SDK exposes, is on each provider's page, under Errors:

  • Gmail: Google's error format.
  • Linear: GraphQL errors with Linear's extensions.

A generic body is the fallback, decided case by case: for a provider whose SDK has no error format to match, and for custom providers that don't set one. REST: { error: { code, message, action?, rules? } }. On a GraphQL endpoint, GraphQL's shape, so clients still surface the message: { errors: [{ message, extensions: { code, action?, rules? } }] }. unauthenticated and unknown_provider happen before the gateway knows the provider, so they always use the generic REST body.

The gateway's error codes. Each provider page says where the code goes in its shape:

StatuscodeWhen
401unauthenticatedMissing, malformed, wrongly signed or expired session token.
403policy_deniedThe policy or a check denied the call. message is the reason.
404, 405, 400policy_deniedNo such action, method or argument, or an ambiguous path.
400bad_requestMethod-override header, wrong GraphQL content type.
413bad_requestBody over maxBodyBytes (10 MB by default).
403unknown_providerThe token's provider isn't on this gateway.
502upstream_authThe credential source failed.
502upstream_unreachableThe provider couldn't be reached.

Custom providers

Set formatError to answer in your provider's own shape. It gets the status, the code from the table above, the prefixed message and, for denials, the action and rule ids.

import { createProvider, type GatewayError } from "@waterlock/gateway";

createProvider<AcmeRequest>({
  id: "acme",
  // ...
  formatError: (e: GatewayError) =>
    Response.json({ code: e.status, message: e.message }, { status: e.status }),
});