Response & deny codes
HTTP statuses and the canonical platform deny wire-codes.
When the platform denies a request it returns stable base fields: a
human-readable error plus a machine-readable code. Limit decisions may also
include limitCode and an _fs envelope with reaction, retry safety, origin,
decision id, and limit metadata. Branch on typed SDK helpers and code, never
the message. The frontend SDK exposes the canonical gateway vocabulary as
FS_DENY_CODES.
{ "error": "Rate limit reached for this key.", "code": "rate_limited" }
import {
FartherShoreApiError,
FS_DENY_CODES,
} from "@farthershore/farthershore-js";
try {
await fs.route.get("/v1/cron-jobs");
} catch (err) {
if (err instanceof FartherShoreApiError) {
switch (err.code) {
case FS_DENY_CODES.credit_exhausted:
return showFundingExhausted();
case "route_not_enabled":
return promptUpgrade();
}
}
}
Platform deny wire-codes (FS_DENY_CODES)
The canonical deny code values. Grouped by concern; the HTTP status the
platform returns alongside each is in the table.
code | HTTP | Meaning |
|---|---|---|
limit_exceeded | 402 | A usage/quota limit on a metered dimension is reached. |
rate_limited | 429 | A per-window rate limit is reached. Retryable. |
credit_exhausted | 402 | On a block plan, available funding cannot cover the request's evaluated economic maximum (owner: the monetary reservation). |
credit_state_unavailable | 503 | A block plan's funding projection is absent or unreadable — fails closed before allocation. Retryable. |
enforcement_denied | 402 | A consume-phase batch enforcement check denied the request. |
commercial_release_unprovable | 503 | The per-business commercial-release bundle is absent, incomplete, hash-invalid, scope-mismatched, or the emergency budget is exhausted. Retryable. |
admission_descriptor_unavailable | 503 | The route's admission descriptor is missing, corrupt, or from the wrong release / rating context. Retryable. |
admission_descriptor_no_admissible_tuple | 503 | No conditional dimension tuple is consistent with the request. Retryable. |
invalid_admission_knob | 422 | The client's output knob (max_tokens and aliases) is malformed or has conflicting aliases. Fix the request. |
admission_bound_exceeded | 422 | The client's output knob exceeds the route's declared bound and cannot be safely rewritten. Lower the knob. |
limit_allocator_unavailable | 503 | A limit check was transiently unavailable. Retryable. |
route_not_enabled | 403 | The active plan does not grant the matched route identity. |
invalid_entitlement_shape | 503 | The resolved plan access failed schema validation. Retryable. |
unsupported_constraint_schema | 503 | A limit rule used an unsupported schema. Retryable. |
enforcement_error | 500 | Enforcement hit an unexpected error. |
enforcement_dependency_unavailable | 503 | An enforcement dependency was transiently unavailable. Retryable. |
concurrency_limit_exceeded | 429 | The plan's concurrent-request cap is reached. Retryable. |
concurrency_context_unavailable | 503 | Concurrency context couldn't be read. Retryable. |
concurrency_coordinator_unavailable | 503 | The concurrency coordinator was unavailable. Retryable. |
key_expired | 401 | The API key has expired. |
credential_revoked | 401 | The credential was withdrawn (revoked, or its owning subscription, plan or business was removed). Not retryable; a new credential is required. |
credential_rotated | 401 | The credential was superseded by a rotation. Re-read the stored secret and retry with the new one. |
credential_env_reset | 401 | The preview environment was rebuilt from a new contract, which deletes its subscriptions and their keys. Re-subscribe, or bootstrap a new persona. |
permission_denied | 403 | The member's resolved permissions do not satisfy the route requirement. |
permission_unresolved | 403 | RBAC is enabled but the credential has no usable permission claim. |
geo_context_unavailable | 503 | Geo context couldn't be resolved. Retryable. |
geo_blocked | 403 | The request origin is in a blocked region. |
geo_not_allowed | 403 | The request origin isn't in the allow-list. |
resource_count_limit_exceeded | 402 | A counted-resource cap (e.g. cron_jobs) is reached. Core atomically authorizes the create and the gateway relays the denial. |
post_stream_overspend | 402 | Previously reported streaming usage crossed a blocking quota; later requests remain locked out until reset. |
resolver_rate_limited | 429 | An internal resolver was rate-limited. Retryable. |
resolver_unavailable | 503 | An internal resolver was unavailable. Retryable. |
credential_resolver_miss_rate_limited | 429 | Credential-resolver miss path was rate-limited. Retryable. |
request_too_large | 413 | A per-request capacity/payload ceiling was exceeded; modify the request (capacity class, not retryable). |
provider_throttled | 503 | A relayed upstream-provider throttle (adaptive class, limitOrigin: provider). Retryable. |
Retryability is decision-specific
Use the public guards on the caught error, not a raw status or code.
_fs.reaction and _fs.retrySafe are authoritative when present, so a
structured decision can make a nominal 429/503 unsafe to replay. Only when no
structured envelope exists does the SDK fall back to the transient code/status
classification below.
import { isRetryable, isThrottled } from "@farthershore/farthershore-js/errors";
try {
await fs.route.get("/v1/cron-jobs");
} catch (err) {
if (!isReplaySafeOperation()) throw err;
if (isThrottled(err)) return backOffAndRetry(); // honor Retry-After
if (isRetryable(err)) return retryWithBackoff(); // transient fallback
throw err;
}
Fallback-transient codes are limit_allocator_unavailable, rate_limited, invalid_entitlement_shape,
unsupported_constraint_schema, enforcement_dependency_unavailable,
concurrency_limit_exceeded, concurrency_context_unavailable,
concurrency_coordinator_unavailable, geo_context_unavailable,
resolver_rate_limited, resolver_unavailable,
credential_resolver_miss_rate_limited, provider_throttled.
limit_exceeded and credit_exhausted are not retryable — the limit
won't clear by retrying. Surface an upgrade or funding affordance instead (see
the limitCode field below). invalid_admission_knob and
admission_bound_exceeded (422) mean the request itself must change.
Never automatically replay a mutation unless its idempotency contract proves the same request cannot duplicate the effect.
Upgrade affordance — limitCode
A limit deny also carries a separate limitCode field (an upgrade-affordance
value, not a wire code) telling the UI what kind of limit was hit. The fixed
values:
limitCode | Hit |
|---|---|
quota | An included-usage / hard-cap quota. |
rate_limit | A per-window rate limit. |
credit | Funding exhaustion on a block plan (credit_exhausted). |
resource:<name> | A counted-resource cap, e.g. resource:cron_jobs (open family — match by the resource: prefix). |
Runtime verification statuses
These come from the upstream's @farthershore/backend,
not the platform deny path — when fs.verifyRequest() / fs.middleware() rejects
a request the platform forwarded. Verification is fail-closed: every failure
maps to one status.
| HTTP | When |
|---|---|
401 | Any verification failure — missing / malformed / bad-signature / stale / clock-skew / wrong-route / body-hash-mismatch / replayed-nonce / unknown-kid / jwks-unavailable. |
403 | Verified principal is the wrong subject or credential surface, or fails an application permission check (member_subject_required, service_subject_required, surface_not_allowed). |
413 | The request body exceeds MAX_BODY_BYTES. |
FartherShoreError.code carries the precise runtime reason (for example,
invalid_token or jwks_unavailable). statusForCode(code) supplies the
default mapping and explicitly handles body_too_large and
surface_not_allowed; subject and permission helpers construct their own 403
errors. There is no fail-open branch.
Origin availability — origin_unavailable and origin_timeout
These two codes describe the hop between the gateway and your backend. They
are always the platform's own envelope — the gateway never relays your hosting
provider's error page (Railway's Application not found, a raw HTML 502), so a
caller can always tell "the origin is not there" from "your application said
no". Neither is ever billed to the customer, and any reservation the request
took is released.
| HTTP | code | When |
|---|---|---|
503 | origin_unavailable | The environment has no usable backend origin, or the origin could not be reached — connection refused, DNS/TLS failure, or a hosting-edge 5xx page. |
504 | origin_timeout | The origin did not produce response headers within the route's time-to-first-byte budget. |
Both carry Retry-After and an _fs envelope with
{ "retrySafe": true, "reaction": "backoff_retry" }:
{
"error": "Origin unavailable",
"code": "origin_unavailable",
"_fs": { "retrySafe": true, "reaction": "backoff_retry" }
}
Your backend's OWN responses are never rewritten. A JSON body your application
returns — including a 404 or a 503 — is relayed verbatim; only a
signature-less hosting-edge error page is reclassified. A provider cap your
backend reports (for example 413 {"error":"too_many_pages"}) keeps its body
too: the platform merges its _fs telemetry into your document rather than
replacing it.
Other stable operational codes
Not every stable code is a member of the closed gateway-denial catalog.
origin_unavailable is a gateway routing failure: the selected environment has
no usable backend origin and returns 503 without falling back to production.
Backend verification and authorization also use stable runtime codes such as
context_unverified, principal_required, member_subject_required,
service_subject_required, and surface_not_allowed. Diagnose those at the
signature/principal/application boundary rather than adding them to
FS_DENY_CODES. A usage payload from an outdated @farthershore/backend that
lacks the served-identity block receives a permanent 410/422 unsupported_usage_schema with expected_schema_version and
received_schema_version — upgrade the SDK; the event is never queued,
quarantined, or backfilled.
HTTP status taxonomy
How the statuses map to categories across both surfaces:
| HTTP | Category | Typical codes |
|---|---|---|
401 | Authentication | key_expired, credential_revoked / credential_rotated / credential_env_reset, runtime verification failures |
402 | Funding / quota | credit_exhausted, limit_exceeded, post_stream_overspend, enforcement_denied, resource create limits |
403 | Authorization | route_not_enabled, permission denies, geo denies, resource pre-request limits |
413 | Payload | request_too_large (oversized body / capacity) |
422 | Admission knob | invalid_admission_knob, admission_bound_exceeded (modify the request) |
429 | Rate / throughput | rate_limited, concurrency_limit_exceeded (customer wait-for-slot) |
503 | Origin availability | origin_unavailable (no usable / unreachable backend origin) |
504 | Origin availability | origin_timeout (no response headers within the route budget) |
500 | Runtime | enforcement_error |
503 | Transient runtime | *_unavailable, *_rate_limited, commercial_release_unprovable (retryable, fail-closed) |
How to debug a denial
- Read the
code(not the message). - Call
isRetryable(err)and require replay-safe operation semantics. Honor the structured decision andRetry-Afterbefore retrying. - If it's a limit (
limit_exceeded/credit_exhausted/resource_count_limit_exceeded), readlimitCodeand surface an upgrade or funding affordance. - If it's a 422 admission code, the client's output knob is malformed or above the route's declared bound — see Monetary admission.
- If it's a 401 from your own upstream, it's a verification failure — check
FartherShoreError.codeand thatFS_RUNTIME_TOKENis current. - Confirm the subscriber's key, subscription, and plan limits.