rekhu

Packages and API

Errors

Every error the core packages throw, in Laravel and Node.js, with when it happens and what to do.

How errors work

When Rekhu refuses a call, the core package throws. Every refusal is one base class with a class of its own under it, so you catch exactly what you handle:

  • Laravel: Rekhu\Core\Exceptions\RekhuException, with the classes under Rekhu\Core\Exceptions.
  • Node.js: RekhuError, with the classes exported from @rekhu/core.

Each carries status (the HTTP status Rekhu gave, or 0 when the call was not made) and type (Rekhu's error type), and a message that says what was wrong.

The error types

Class (both packages) Status Type When What to do
InvalidKey 401 no key, a revoked key, or a key made on another Rekhu platform put the app key from the project's Connection tab in REKHU_APP_KEY
ModuleNotEnabled 403 module_not_enabled the knower's module is off for this application call modules() and show only what may be used
RegionUnavailable 403 region_unavailable the account is set to Europe only, and this call has no engine in the EU see Privacy and data region
CreditExhausted 402 the account's credit is used up top up; see Credit and usage
AppPaused 402 app_limit the application is paused, or at its monthly limit resume it, or raise the limit, in the account
FileNotFound 404 a kept file is past its day, removed, or of another account send the file again
InvalidRequest 422, 400 refused before anything ran: an unknown order key, fix or option, a font not in the library read the message, which names what was wrong; nothing is charged
UnsafeFile 422 refused_unsafe the file is not safe; safety holds the report tell the customer why, ask for another file; see File safety
RateLimited 429 too many calls at once, after the package tried again wait, and try again later
Unavailable 502, 503 connection_error when Rekhu could not be reached Rekhu, or a service behind it, could not answer after the package tried again try again in a moment
LimitReached 429 limit_reached one of your own limits a day; scope is tenant or user; nothing was sent to Rekhu tell the user, or raise the limit in your config
RekhuException / RekhuError any configuration_error, timeout, upload_failed, … any other refusal, such as a knower not in REKHU_KNOWERS, or an upload still being checked read the message

UnsafeFile carries safety, and LimitReached carries scope. Both extend the base class, so a catch of the base class catches them too.

Catch what you handle

use Rekhu\Core\Exceptions\CreditExhausted;
use Rekhu\Core\Exceptions\InvalidRequest;
use Rekhu\Core\Exceptions\LimitReached;
use Rekhu\Core\Exceptions\RekhuException;
use Rekhu\Core\Exceptions\UnsafeFile;
use Rekhu\Core\Rekhu;

try {
    $check = Rekhu::seshat()->for($user)->check($request->file('artwork'), $order);
} catch (UnsafeFile $e) {
    return back()->withErrors(['artwork' => $e->getMessage()]);       // $e->safety holds the report
} catch (LimitReached $e) {
    return back()->withErrors(['artwork' => 'Today\'s checks are used up.']);   // $e->scope: tenant or user
} catch (InvalidRequest $e) {
    report($e);                                                          // a mistake in the order or the fixes
    throw $e;
} catch (CreditExhausted $e) {
    Log::critical('Rekhu credit is used up');
    return back()->withErrors(['artwork' => 'Checking is not available right now.']);
} catch (RekhuException $e) {
    Log::error('Rekhu refused', ['status' => $e->status, 'type' => $e->type, 'message' => $e->getMessage()]);
    throw $e;
}
import { CreditExhausted, InvalidRequest, LimitReached, RekhuError, UnsafeFile } from '@rekhu/core';

try {
    const check = await rekhu.seshat({ user }).check(buffer, { name: 'flyer.pdf', order });
    res.json(check);
} catch (error) {
    if (error instanceof UnsafeFile) return res.status(422).json({ message: error.message, safety: error.safety });
    if (error instanceof LimitReached) return res.status(429).json({ message: 'Today\'s checks are used up.', scope: error.scope });
    if (error instanceof InvalidRequest) throw error;               // a mistake in the order or the fixes
    if (error instanceof CreditExhausted) {
        logger.error('Rekhu credit is used up');
        return res.status(503).json({ message: 'Checking is not available right now.' });
    }
    if (error instanceof RekhuError) logger.error('Rekhu refused', { status: error.status, type: error.type, message: error.message });
    throw error;
}

Catch the specific classes before the base class: the first catch that matches wins.

Retries and double charges

The packages try a call again by themselves, so most short failures never reach your code:

  1. When Rekhu cannot be reached. A dropped connection, or a 502, 503 or 504 that does not come from Rekhu itself (for example for a moment while Rekhu is being updated), is tried up to four times, waiting 2, 4 and 8 seconds.
  2. When Rekhu is busy. A 429 or 529, or an answer of type overloaded_error or rate_limit_error, is tried up to three times, waiting as long as Rekhu's retry-after asks, up to 5 seconds.
  3. What is not. Every other failure is thrown at once as the class for it.
  4. No double charges. Every charged call carries an idempotency key. When a retry reaches Rekhu after the first call already ran, Rekhu answers the first result again, with meta.replayed, and charges nothing.
  5. Parts of a large file. Each part of an upload in parts is sent again up to three times on its own. When it still fails, resume the upload; see Large files.

What is never charged

A refused call costs nothing: an InvalidRequest, an UnsafeFile, a ModuleNotEnabled, and a LimitReached, which is never sent. A fix with nothing to do is not charged either.

Errors in the assistant

The assistant packages handle Rekhu's refusals themselves. When the project's assistant is paused, at its monthly limit, or out of credit, your users read that the assistant is not available right now, and your app's log says why. In Laravel, php artisan rekhu:assistant checks the key and the credit; in Node.js, assistant.check() does.

The chat routes answer errors as JSON with a message:

Status Meaning
401 the user's sign-in expired, or user() returned null
403 access or accessUsing refused this user, or (Node.js) a request without the X-Rekhu-Protocol header
419 the CSRF token expired (Laravel, cookie sign-in)
429 a cap was reached, such as the public chat's caps per visitor

The widget shows the message, and asks the user to reload on 401 and 419.

Next