CEEJAYDocs

Errors and retries

What a call can throw, what the SDK retries automatically, and what it leaves to you.

The errors

ErrorWhenFields
CeejayErrorCEEJAY responded with an error: any status outside 200 to 299status, message, code, retryAfter, requestId, body
CeejayConnectionErrorNo response was received: the network failed, or the call took longer than timeoutMsmessage, timedOut, cause
TypeErrorAn argument is invalid, such as a number for a member ID. It is thrown before anything is sentmessage

A fourth, CeejaySignatureError, belongs to the two signature checks. It is on Verifying signatures.

The errors
import { CeejayConnectionError, CeejayError } from '@communitystudios/ceejay'

try {
  await ceejay.economy.deduct({ discordId: '409876543210987654', amount: 100 })
} catch (error) {
  if (error instanceof CeejayError) {
    // CEEJAY answered, and refused or failed.
    console.log(error.status, error.code, error.message, error.requestId)
    console.log(error.body) // the whole answer, for the extra fields some refusals carry
  } else if (error instanceof CeejayConnectionError) {
    // No answer came back. A write may or may not have been applied.
    console.log(error.message, error.timedOut)
  } else {
    throw error
  }
}

The fields of a CeejayError:

Prop

Type

Check the code, not the status or the message. The message text can change. A code does not: the list of codes is closed for version 1 of the API, and a code is never renamed or removed. Two errors of the same kind can carry different statuses, for a reason given under Statuses, so the status alone does not identify which rule failed.

Your key is never part of an error, whatever a server puts in its response. The SDK redacts your key from the response before it reads the message, the code or the body. The cause of a CeejayConnectionError is a plain copy of what the fetch threw: its name, its message and its code, and no other properties.

Errors every request can raise

These are the same for every request. The first six come before a request reaches its own checks, in this order.

StatuscodeCause
401missing_api_keyThe request carried no key
401invalid_api_keyThe key is not in the right format, or is not known
401api_key_revokedThe key was revoked
401api_key_expiredThe key is past its expiry date
429rate_limitedThe key has used its requests for this minute. retryAfter says how many seconds to wait
403insufficient_scopeThe key does not have the permission the request needs. error.body lists required and granted
405method_not_allowedThe request was sent with the wrong method. The SDK always sends the right one
400bad_requestAn argument was rejected. error.body.field names the field, when one field is at fault
500internal_errorSomething failed on CEEJAY's side

A key that is missing, or does not start with ceejay_sk_, never leaves your server: the SDK throws a TypeError when the client is created.

The rate limit is checked before the permission. A key that is over its limit gets 429 from every request, also from one it has no permission for, and that request still counts. So a 429 does not tell you the key is permitted to make the call.

Two responses are generated before CEEJAY's own code runs, and carry no code and no requestId: a body larger than 1 MB (413, with the text Body exceeded 1mb limit), and a body that is not JSON. The SDK always sends JSON, so the second cannot happen through it. The SDK gives the first the code body_too_large, as it gives it to the 413 of ceejay.players.setFields and ceejay.players.logEvents for a body over 512 KB.

The rate limit

A key has a rate limit in requests per minute: 60 unless the key was given a different limit, and 600 at most. ceejay.community.get reports it as key.rateLimitPerMinute.

Every response to a request made with a known key carries three headers, on success and on error:

HeaderWhat it is
X-RateLimit-LimitThe key's limit, in requests per minute
X-RateLimit-RemainingHow many requests are left in the current minute. Never below 0
X-RateLimit-ResetWhole seconds until the current minute ends, from 1 to 60

A 401 carries none of them: no key is known, so there is no limit to report.

The SDK does not expose these three headers to your code. It reads what it needs from a 429: the wait, which is on the error as retryAfter. To see how much of its rate limit a key has left, call ceejay.community.get for its limit and count your own requests.

Statuses

StatusWhat it means
400An argument was rejected
401No key, or a key that is not accepted
403Not allowed
404The resource does not exist, or this instance does not support the feature yet
409The request conflicts with a business rule
429Too many requests
503A service CEEJAY depends on, such as Discord, was unavailable. Nothing was changed

Four errors return 400 for backward compatibility, although the status for a business rule is 409: quest_not_active, quest_already_completed and quest_limit_reached on quests.complete, and insufficient_balance on economy.deduct. The other rules of a quest return 409. This is why you check the code.

Three more errors also keep their original status for backward compatibility: bot_unavailable and bot_timeout, with 502 or 504, when CEEJAY's bot could not finish a request, and discord_error, which passes Discord's own status through.

Codes

Every code of version 1 of the API. The page for each area lists the codes its own functions return, and what else is in error.body.

codeStatusRaised by
missing_api_key, invalid_api_key, api_key_revoked, api_key_expired401Every request
insufficient_scope403Every request but community.get
method_not_allowed405Every request
rate_limited429Every request
internal_error500Every request
bad_request400Every request that takes an argument
not_available404A request whose feature this instance does not support yet. error.body.feature names it, and community.get reports the same names
member_not_found404members.get and xp.deduct, for a member CEEJAY has no profile for. The grants, quests.complete, members.link and the two role changes, for an ID that is not a member of your server
discord_unavailable503The grants, quests.complete, quests.reportTask and members.link, when CEEJAY could not query Discord about the member. Nothing was changed
idempotency_key_reused409Every write that takes a key, for a key that was already used for a different request
idempotency_in_progress409Every write that takes a key, while the first request with the key is still running. retryAfter is 5
idempotency_outcome_unknown409messages.send, events.emit and quests.reportTask, when CEEJAY cannot tell whether an earlier attempt with the key was applied
idempotency_unavailable503Every write that takes a key, when you send one and this instance cannot store keys yet
unknown_platform400members.link and quests.reportTask, for a platform that is not known
linked_by_sign_in, player_already_linked409members.link
xp_limit_reached409The XP grants and quests.complete, for a total that would not fit
balance_limit_reached409economy.grant and quests.complete, for a total that would not fit
insufficient_balance400economy.deduct
program_not_found, member_not_enrolled404xp.program.grant and xp.program.deduct
program_not_active, member_status_not_allowed409The same two
quest_not_found404quests.complete
quest_not_active, quest_already_completed, quest_limit_reached400quests.complete, for three of the quest's rules
quest_not_started, quest_ended, quest_requires_role, quest_requires_level, quest_requires_season_xp, quest_requires_program, quest_full, quest_cooling_down, quest_requires_quest, quest_daily_limit_reached409quests.complete, for the other rules of the quest
member_roles_unavailable503quests.complete, when the quest requires a role and the member's roles could not be read
role_not_found404roles.grant and roles.revoke
role_not_allowed, bot_missing_permission403The same two. messages.send returns the second as well
roles_unavailable, dashboard_roles_unavailable503roles.list, roles.grant and roles.revoke, when a list they need could not be read
channel_not_found404messages.send
channels_unavailable503channels.list
discord_rate_limited429The requests that go through Discord, when Discord rate-limits the bot
discord_not_configured500The same requests, on an instance that cannot reach Discord yet
discord_errorDiscord's ownThe same requests, for any other error from Discord. error.body.details contains Discord's response
purchase_not_found404shop.completePurchase
wrong_status409shop.completePurchase, for a purchase whose status has already changed
bot_unavailable502quests.reportTask and shop.completePurchase, when CEEJAY's bot could not be reached or rejected the request. On quests.reportTask nothing was changed
bot_timeout502 or 504The same two, when CEEJAY's bot did not respond in time. On quests.reportTask also when the bot's response was lost, so the task may or may not have been completed

Idempotency keys

Every write takes one more option, idempotencyKey, but for the three writes of Connect, which take none: each of those is safe to retry as it is. The key is a string you choose, 1 to 128 characters, for one request. The SDK sends it in the body as idempotencyKey.

  • When the same request is sent again with its key, nothing is repeated. The response is the original one, with duplicate: true.
  • Each key identifies one request. Two requests for one event need two keys, for example match-9:coins and match-9:xp. The same key on another function, or with other values, is rejected with idempotency_key_reused.
  • A key is stored only when its request was applied. A rejected request can be sent again with the same key once the cause is fixed.
  • A key belongs to the API key that sent it, and is retained for 30 days.

Generate the key from something that identifies the request in your own system, such as an order ID or a match ID. A key generated from the time of the call is new on every attempt, and protects nothing.

The errors an idempotency key can produce:

codeStatusWhat it meansWhat to do
idempotency_key_reused409The key was already used for a different request: another function, or other valuesUse a new key for a new request
idempotency_in_progress409The first request with this key is still runningNothing. The SDK waits the retryAfter of the response, 5 seconds, and retries
idempotency_outcome_unknown409An earlier attempt with this key may or may not have been applied, and CEEJAY did not apply it againFind out whether it was applied, then send it with a new key if it was not
idempotency_unavailable503This instance cannot store keys yet. Nothing was changedContact support to complete the update. ceejay.community.get shows it as features.retryKeys.available

idempotency_outcome_unknown applies to the three writes whose effect is outside CEEJAY and cannot be reversed or looked up: a posted message, a sent event and a reported quest task. After an attempt that got no response, CEEJAY retains the key and does not post, send or report a second time. The other writes are either applied in full or not at all, or are safe to apply twice, so a retry of them returns the original response.

What the SDK retries

After a 429, a 5xx or no response, the SDK waits and retries the request. It only does this when it is safe to retry:

  • A read is retried.
  • A write sent with idempotencyKey is retried.
  • A write without a key is never retried. CEEJAY would apply it twice.
FunctionRetried by the SDK
community.get, members.get, xp.getBalance, economy.getBalance, quests.list, roles.list, channels.list, programs.list, shop.listPurchases, connect.getSession, connect.listSessionsYes
members.link, xp.grant, xp.deduct, xp.program.grant, xp.program.deduct, economy.grant, economy.deduct, quests.complete, quests.reportTask, roles.grant, roles.revoke, messages.send, events.emit, shop.completePurchaseOnly with idempotencyKey
connect.createPass, connect.createSession, connect.confirmSessionNo. They take no key. Retrying one yourself is safe

One more response is waited for and retried, for a write with a key: a 409 with the code idempotency_in_progress. It means the first request with that key is still running. The SDK can receive this when it retries after a dropped connection. Waiting returns the original response.

These are never retried, whatever the request:

  • A status from 400 to 499 other than 429, with the one exception above. The request itself is invalid, and would be rejected again.
  • A 409 with the code idempotency_outcome_unknown or idempotency_key_reused. Waiting changes neither.
  • A 503 with the code idempotency_unavailable. This instance cannot store keys yet, and waiting does not change that.

How long it waits

SettingDefaultWhat it is
maxRetries2How many times to retry after the first attempt. From 0 to 10
timeoutMs30000How long to wait for a response to one attempt, in milliseconds. 2147483647 at most, which is about 24 days: a timer cannot run for longer, and a larger number is rejected with a TypeError

Between two attempts the SDK waits as long as retryAfter in the response specifies. When the response has no retryAfter, it uses exponential backoff: the first wait is between 250 and 500 milliseconds, and each wait after that is twice as long, up to 8 seconds.

A wait of more than 60 seconds is not waited for. The error is thrown immediately, with retryAfter set, so your own code can decide.

Set both for every call when you create the client, or for one call in its last argument:

How long it waits
import { Ceejay } from '@communitystudios/ceejay'

const patient = new Ceejay({ apiKey: process.env.CEEJAY_API_KEY, maxRetries: 4, timeoutMs: 10_000 })

// One call that must answer quickly or not at all.
const member = await patient.members.get('409876543210987654', { maxRetries: 0, timeoutMs: 2_000 })

console.log(member.level)

A write that got no response

After a CeejayConnectionError, a write may or may not have been applied.

  • With idempotencyKey, the SDK has already retried it with the same key. If it still fails, retry it later with the same key: it is applied once.
  • Without a key, read before you write again. After a grant of XP that timed out, call ceejay.xp.getBalance and compare the total with what you expect.

A request that the rate limit rejected changed nothing, so it is always safe to retry. With a key, the SDK does that for you:

A write that got no response
const grant = await ceejay.xp.grant(
  { discordId: '409876543210987654', amount: 250, reason: 'Won match 881' },
  { idempotencyKey: 'match-881:xp', maxRetries: 4 },
)

console.log(grant.xp, grant.duplicate)

Discord's own limit

roles.list, roles.grant, roles.revoke, channels.list and messages.send go through Discord, and one bot serves every community on an instance. When Discord rate-limits the bot, the call fails with status 429 and the code discord_rate_limited. retryAfter is Discord's wait in whole seconds, when Discord provided one. This is not your key's own rate limit: that is rate_limited.

Do not retry these in a tight loop: Discord can block the bot for a while, for every feature.

A member who has only just joined your server can take up to 15 seconds to be found. After a 404 with the code member_not_found for such a member, wait 15 seconds before you retry.

Stopping a call

Pass an AbortSignal as signal. When it is aborted the call stops and throws the reason of the signal, not one of the errors above.

Stopping a call
const controller = new AbortController()
setTimeout(() => controller.abort(), 1_500)

const page = await ceejay.quests.list({ limit: 100 }, { signal: controller.signal })
console.log(page.total)

When you ask us for help

Send us the requestId of the error, and the time and the function you called. CEEJAY logs the URL path, method, status code and timestamp of every request made with a key it accepts.

Every request the SDK sends carries the header User-Agent: ceejay-sdk-typescript/0.1.0, with the version of the SDK you use.

On this page

Community StudiosPrivacyTerms