Response & deny codes
HTTP statuses and the canonical gateway deny wire-codes.
HTTP statuses and the canonical gateway deny wire-codes.
When the gateway denies a request it returns a flat JSON body: a human-readable
error plus a machine-readable code. Branch on the code — the message
can change between releases, the code can't. The frontend SDK exposes the same
vocabulary as FS_DENY_CODES on @farthershore/farthershore-js, so client logic
and support workflows share one source of truth.
{ "error": "Rate limit reached for this key.", "code": "rate_limited" }
import { FartherShoreApiError, FS_DENY_CODES } from "@farthershore/farthershore-js";
try {
await fs.feature("cron-jobs").json("/v1/cron-jobs");
} catch (err) {
if (err instanceof FartherShoreApiError) {
switch (err.code) {
case FS_DENY_CODES.rate_limited: return backOffAndRetry();
case FS_DENY_CODES.credit_exhausted: return promptTopUp();
case FS_DENY_CODES.feature_not_enabled: return promptUpgrade();
}
}
}
FS_DENY_CODES)The canonical deny code values. Grouped by concern; the HTTP status the
gateway returns alongside each is in the table.
code | HTTP | Meaning |
|---|---|---|
limit_exceeded | 429 | A usage/quota limit on a metered dimension is reached. |
rate_limited | 429 | A per-window rate limit is reached. Retryable. |
credit_exhausted | 402 | Prepaid credit balance is below what this request would spend. |
enforcement_denied | 403 | A consume-phase batch enforcement check denied the request. |
limit_allocator_unavailable | 503 | The limit allocator was transiently unavailable. Retryable. |
feature_not_enabled | 403 | The plan/entitlement doesn't grant the requested feature. |
invalid_entitlement_shape | 503 | The resolved entitlement failed schema validation. Retryable. |
unsupported_constraint_schema | 503 | A constraint 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. |
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 | 429 | A counted-resource cap (e.g. cron_jobs) is reached. |
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. |
This subset is safe to auto-retry with backoff — the 429 throttles plus the
transient 503 dependency faults. The public guards take the caught error, not
a raw code: isRetryable(err) is true for any of these denies (it also folds in
transient transport statuses), and isThrottled(err) narrows to the 429 back-off
cases.
import { isRetryable, isThrottled } from "@farthershore/farthershore-js";
try {
await fs.feature("cron-jobs").json("/v1/cron-jobs");
} catch (err) {
if (isThrottled(err)) return backOffAndRetry(); // 429 rate/quota
if (isRetryable(err)) return retry(); // + transient 503 deps
throw err;
}
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.
limit_exceeded and credit_exhausted are not retryable — the limit won't
clear by retrying. Surface an upgrade or top-up affordance instead (see the
limitCode field below).
limitCodeA 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 | The prepaid credit balance. |
resource:<name> | A counted-resource cap, e.g. resource:cron_jobs (open family — match by the resource: prefix). |
These come from the upstream's @farthershore/backend,
not the gateway deny path — when fs.verifyRequest() / fs.middleware() rejects
a request the gateway 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. |
413 | The request body exceeds MAX_BODY_BYTES. |
FartherShoreError.code carries the precise runtime reason (e.g.
invalid_token, jwks_unavailable); statusForCode(code) maps it to the HTTP
status above. There is no fail-open branch.
How the statuses map to categories across both surfaces:
| HTTP | Category | Typical codes |
|---|---|---|
401 | Authentication | key_expired, runtime verification failures |
402 | Credits | credit_exhausted |
403 | Authorization | feature_not_enabled, enforcement_denied, geo_blocked, geo_not_allowed |
413 | Payload | runtime oversized body |
429 | Limits / throttle | limit_exceeded, rate_limited, concurrency_limit_exceeded, resource_count_limit_exceeded |
500 | Runtime | enforcement_error |
503 | Transient runtime | *_unavailable, *_rate_limited (retryable) |
code (not the message).limit_exceeded / credit_exhausted / resource_count_limit_exceeded), read limitCode and surface upgrade / top-up.FartherShoreError.code and that FS_RUNTIME_TOKEN is current.