asobi_error (asobi v0.75.1)

View Source

The one error object.

Every failure asobi reports - REST, WebSocket, and the ops/RPC surfaces built on top of them - is describable as:

{"error": {"code": "storage.not_found", "message": "...", "details": {}}}

code is the contract. It is machine-readable, namespaced by domain (storage., save., auth., social., match., world., chat., matchmaker. and the rest) or bare when it is cross-cutting (rate_limited, internal), and drawn from the closed set in codes/0 - a client may branch on it. message is prose for a human reading a log; it may be reworded at any time and must not be parsed. details is always a map, #{} when there is nothing to add, so no client needs a null branch.

The code set is closed on purpose: script- and client-supplied strings never become codes. An unrecognised reason is reported under a known code with the raw string in details.

An installed extension widens the set by declaring codes/0 in its manifest (see asobi_extension). That set is read once, at asobi_extensions:resolve/0, from validated manifests, so the set is still closed per deployment: nothing reachable from a request can add to it. Core's own codes are core_codes/0, which is what asobi_extension_reserved derives core's reserved namespaces from - an extension may only mint codes in a domain it owns.

Codes carry their HTTP status (status/1), so a REST controller states the failure and never the number:

{asobi_error, ~"storage.not_found"}
{asobi_error, ~"save.version_conflict", #{current_version => 4}}

register_handler/0 installs handle/3 as the Nova return handler for those tuples.

A handful of routes answered with more than error before the object existed (fields, errors, retry_after, field). Those keys stay exactly where they were and are also the object's details, so an existing client keeps working and a new one reads one place: see legacy/2.

Summary

Functions

Every defined code: core's, plus every installed extension's.

The codes asobi itself defines.

Whether Code is defined by core or by an installed extension.

The error object for a WebSocket reason string.

Nova return handler for {asobi_error, ...} controller results.

A Nova {json, ...} result carrying the object for Code plus Extra.

legacy/2's body, for a caller that writes the response itself.

The registered human-readable message for Code.

The error object for Code, with no details.

The error object for Code, carrying Details.

The error object for Code with the message overridden.

Install handle/3 as Nova's handler for {asobi_error, ...} results.

The HTTP status for Code. An undefined code is a server bug: 500.

The WebSocket reason -> code mapping, as {Reason, Code} pairs.

Types

code()

-type code() :: binary().

details()

-type details() :: #{atom() | binary() => term()}.

object()

-type object() :: #{error := #{code := code(), message := binary(), details := details()}}.

Functions

codes()

-spec codes() -> [code()].

Every defined code: core's, plus every installed extension's.

The client-facing contract, enumerated. Use core_codes/0 when the question is what asobi itself defines.

core_codes()

-spec core_codes() -> [code()].

The codes asobi itself defines.

asobi_extension_reserved derives core's reserved RPC prefixes from these domains, and must not see extension codes: an extension would then be told it claims a namespace it owns.

defined(Code)

-spec defined(code()) -> boolean().

Whether Code is defined by core or by an installed extension.

For a caller that reports an error on a surface with no Nova return handler to log for it - asobi_rpc - and must not let an undefined code reach a client unnoticed.

from_ws_reason/1

-spec from_ws_reason(atom() | binary()) -> object().

The error object for a WebSocket reason string.

reason is the pre-existing WebSocket error dialect and stays on the wire untouched; this maps it onto a code. A reason with no code of its own becomes ws.request_failed with the raw reason in details - script-supplied strings must not be able to mint codes.

handle/3

-spec handle({asobi_error, code()} |
             {asobi_error, code(), details()} |
             {asobi_error, pos_integer(), code(), details()},
             fun(),
             cowboy_req:req()) ->
                {ok, cowboy_req:req()}.

Nova return handler for {asobi_error, ...} controller results.

{asobi_error, Code} and {asobi_error, Code, Details} take their HTTP status from the code. {asobi_error, Status, Code, Details} overrides it, for the caller that must return a status the code does not imply.

legacy(Code, Extra)

-spec legacy(code(), details()) -> {json, pos_integer(), #{}, map()}.

A Nova {json, ...} result carrying the object for Code plus Extra.

For the routes that answered with more than an error string before the object existed. Extra is those top-level keys, unchanged, so a client reading fields or retry_after keeps working; the same map is the object's details. The status still comes from the code.

legacy_body(Code, Extra)

-spec legacy_body(code(), details()) -> map().

legacy/2's body, for a caller that writes the response itself.

The plugins reply through cowboy_req rather than returning to Nova.

message(Code)

-spec message(code()) -> binary().

The registered human-readable message for Code.

object(Code)

-spec object(code()) -> object().

The error object for Code, with no details.

object(Code, Details)

-spec object(code(), details()) -> object().

The error object for Code, carrying Details.

object(Code, Message, Details)

-spec object(code(), binary(), details()) -> object().

The error object for Code with the message overridden.

For the rare failure whose registered message would hide the one fact the caller needs. Prefer object/2: a per-call message is not part of the contract and cannot be translated or reused.

register_handler()

-spec register_handler() -> ok.

Install handle/3 as Nova's handler for {asobi_error, ...} results.

status(Code)

-spec status(code()) -> pos_integer().

The HTTP status for Code. An undefined code is a server bug: 500.

ws_reasons()

-spec ws_reasons() -> [{binary(), code()}].

The WebSocket reason -> code mapping, as {Reason, Code} pairs.