acyka api

Errors

A phrase name and never a sentence — and why that is what makes a real error type possible.

Every refusal on this api is the same shape:

{
  "message": "errors.oauthInsufficientScope",
  "scope": "lists:write"
}

message is a phrase name. Not a sentence, and not English — one acyka screen can be read in five languages at once, and the server does not get to choose which. Extra fields are merged in beside it wherever a refusal owes a reason: the scope that was missing, how long a ban has left.

That is usually described as a limitation. For a client library it is the opposite: a stable, enumerable set of names is what makes a real type per refusal possible, instead of matching on prose that changes the day somebody rewrites a sentence.

import { Forbidden, NotFound, RateLimited } from '@acyka/api';

try {
  await theirs.library.listMyList();
} catch (err) {
  if (err instanceof Forbidden) {
    // refreshing will not help: the token does not carry it
    ask(err.scope);
  } else if (err instanceof RateLimited) {
    console.log('retry in', err.retryAfter, 'seconds');
  } else if (err instanceof NotFound) {
    return null;
  }
  throw err;
}

401 and 403 are not the same problem #

A 401 means the credential is stale — refresh and try again, which every library does once by itself. A 403 means the token is good and does not carry what this endpoint wants: refreshing produces the same token with the same scopes and earns the same refusal, for ever. A client that retries a 403 is a client in a loop.

The names #

statusnamewhat it means
400errors.badDatathe request was refused before anything looked at it — a status that is not one of the six, a title that is empty
400post.tooLonga post past the one length the site allows
400post.badMarkupa wall of punctuation with no words in it
401errors.unauthorizedno token, an expired one, or one whose application was switched off. Refresh, then retry once.
403errors.oauthInsufficientScopethe token is good and does not carry this. Refreshing will not help; ask for the scope. `scope` names it.
403errors.oauthUserRequiredan application acting for itself asked for something about a person
403errors.suspendedthe account the token acts for is suspended
403errors.noAccessa shelf that is readable and not writable by this person
404errors.notFoundthere is nothing there, or it is not yours to read
404errors.unknownTitleno such title — or one marked 18+ and an account that has not opened the switch
404errors.profileNotFoundno account answers to that name
429common.tooOftenthe minute is spent. `Retry-After` says how long.

The list grows. A name your build has never seen is not an error in itself — read err.code, show your own words for the ones you know, and the name itself for the ones you do not.

// your own words, keyed by the name
const SAYS: Record<string, string> = {
  'errors.oauthInsufficientScope': 'This app has not been allowed that yet.',
  'errors.suspended': 'That account is suspended.'
};

const shown = SAYS[err.code] ?? 'Something went wrong.';

An answer that is not JSON #

A proxy's own 502 is HTML. Every library here turns that into the same ServerError a JSON 502 would have produced, because a parse error at that moment tells a caller nothing about what happened — and being told “the api is having a bad minute” is what they need.