# flare.errors

> A module of flare_mlake's API.

Cross-module typed-error vocabulary for flare.

flare adopts Mojo's **typed errors**
(https://docs.modular.com/mojo/manual/errors/#typed-errors) as the
default error-handling style for new code. Typed errors carry
structured fields, conform to ``Writable`` for ``print()`` /
``String(e)`` rendering, and let callers pattern-match on
condition without ``String(e).startswith(...)`` heuristics.

This module ships the **cross-module** typed errors. Module-local
errors (e.g. :class:`flare.http.template.TemplateError`,
:class:`flare.http.auth_extract.AuthError`,
:class:`flare.http.proxy_protocol.ProxyParseError`) live next to
the parser that raises them, per the Mojo doc's "Define a custom
error type" guidance.

## Convention summary

1. **Prefer ``raises ConcreteError`` over bare ``raises``.**
   Bare ``raises`` erases the error type at compile time (per
   the Mojo doc § "Avoid bare raises with typed errors") so
   callers can no longer access structured fields without
   ``String(e)`` parsing.
2. **One error type per function.** When a function naturally
   raises multiple unrelated conditions, use an
   *enumerated* error type (one struct, ``comptime`` aliases
   for each variant). When each condition needs different
   carried data, use a ``Variant[...]`` of separate structs.
   See :class:`flare.http.template.TemplateError` for the
   enumerated pattern, :class:`flare.http.proxy_protocol.ProxyParseError`
   for the structured-fields-on-one-struct pattern.
3. **Wrap at trait / framework boundaries.** The
   :class:`flare.http.handler.Handler` trait and
   :class:`flare.http.extract.Extractor` trait declare
   bare ``raises`` for backwards compatibility with the original
   surface. Impls that need typed errors internally should
   raise typed errors directly from their bare-raises body — the
   Mojo runtime preserves the typed error's identity (its
   ``Writable`` rendering) even when the function signature is
   bare-raises (see Mojo doc § "Avoid bare raises with typed
   errors" / "type erasure only affects *uncaught* errors").
4. **Don't mix error types in one ``try`` block.** Mojo rejects
   a ``try`` whose body calls functions raising different
   typed-error types. Use sequential / nested ``try`` blocks
   instead.

5. **Two-tier naming.** A type whose name ends ``Error`` names a
   *category* of failure the caller may want to branch on as a group:
   ``HttpError``, ``IoError``, ``NetworkError``, ``ValidationError``,
   ``TlsHandshakeError``, ``TemplateError``. A type named as a bare
   noun names one specific *condition*: ``ConnectionRefused``,
   ``AddressInUse``, ``BrokenPipe``, ``CertificateExpired``,
   ``DatagramTooLarge``, ``TooManyRedirects``. The split mirrors Rust's
   ``io::Error`` versus ``io::ErrorKind``, and it is why
   ``ConnectionTimeout`` (a condition during the connect phase) and
   ``Timeout`` (a condition during any I/O) can both exist in
   :mod:`flare.net.error` without either being redundant.

   Written down in v0.11. The existing names already follow it in the
   main, but not everywhere, and the deviations are what a future
   renaming pass should target -- not the convention.

## What's in this module

- :class:`ValidationError` — invalid input / argument validation
  failures. Carries ``field`` + ``reason`` so callers can
  distinguish per-field failures. ``HttpServer`` maps an uncaught
  ``ValidationError`` to a sanitized ``400 Bad Request`` (see
  :func:`map_handler_error`).
- :class:`HttpStatusError` — a handler-authored error that names the
  exact status to return (mapped through the bare-``raises``
  ``Handler.serve`` boundary by :func:`map_handler_error`).
- :class:`IoError` — I/O-layer failure not covered by the more
  specific :class:`flare.net.NetworkError` subtypes (e.g. a
  generic syscall that returned ``-1`` with a not-otherwise-
  classified errno). Carries ``op`` + ``code`` (errno) +
  ``detail``.

Both types are ``Copyable`` (which implies ``Movable``) and
``Writable``, and shaped per the Mojo typed-errors guidance.

## Structs

### `ValidationError`

```mojo
struct ValidationError
```

Generic input / argument-validation failure.

Use when a function rejects an argument value that fails a
precondition (e.g. ``chunk_size <= 0``, ``port out of range``,
``CSRF token wrong shape``). The ``field`` names *what*
failed; ``reason`` says *why*.

Maps cleanly to a 400 Bad Request response when surfaced
through an HTTP handler — flare's
:class:`flare.http.HttpServer` catches uncaught errors and
sanitises them to a 400 / 500; with this typed error the
handler can map ``field`` to the request field-name in the
error body.

Example:
```mojo
from flare.errors import ValidationError

def validate_chunk_size(n: Int) raises ValidationError:
    if n <= 0:
        raise ValidationError(
            field=String("chunk_size"),
            reason=String("must be > 0, got ") + String(n),
        )
```

Implements: `Copyable`, `Movable`, `Writable`

Fields:

- `field: String`
- `reason: String`

#### `write_to`

```mojo
def write_to[W: Writer](self, mut writer: W)
```

Write ``ValidationError(field): reason`` to ``writer``.

### `HttpStatusError`

```mojo
struct HttpStatusError
```

An error that names the exact HTTP status a handler wants returned.

Raise this from a handler (or anything it calls) to short-circuit
to a specific status instead of the catch-all 500. ``HttpServer``
recognizes it -- even across the bare-``raises`` ``Handler.serve``
boundary -- by its ``Writable`` rendering and maps it to
``status`` with ``message`` as the response body.

Because the message is *deliberately authored by the handler*
(unlike a parser error built from request bytes) it is echoed to
the client regardless of ``ServerConfig.expose_error_messages`` --
it is the escape hatch for intentional, client-facing errors.

Example:
```mojo
from flare.errors import HttpStatusError

def get_user(req: Request) raises -> Response:
    var u = lookup(req.param("id"))
    if not u:
        raise HttpStatusError(status=404, message="user not found")
    return ok(u.value().name)
```

Implements: `Copyable`, `Movable`, `Writable`

Fields:

- `status: Int`
- `message: String`

#### `__init__`

```mojo
def __init__(out self, status: Int)
```

Construct with the canonical reason phrase for ``status``.

#### `write_to`

```mojo
def write_to[W: Writer](self, mut writer: W)
```

Render the defined status-error wire format ``HttpStatusError(<status>): <message>``.

Mojo's ``raises`` erases the concrete error type at the catch
site and offers no typed downcast (and no thread-local
side-channel to plumb one), so the rendered string is the only
transport across the ``Handler.serve`` boundary. This format is
therefore a *defined codec*, not an incidental rendering: it is
produced only here and parsed only by
:func:`parse_status_error`, which the round-trip test pins.

### `MappedHandlerError`

```mojo
struct MappedHandlerError
```

The (status, reason) an uncaught handler error maps to.

Returned by :func:`map_handler_error`; the reactor feeds it to its
error-response builder.

Implements: `Copyable`, `Movable`

Fields:

- `status: Int`
- `reason: String`

### `IoError`

```mojo
struct IoError
```

Generic I/O failure not covered by the more specific :class:`flare.net.NetworkError` family.

Use for local-filesystem syscalls, allocator failures, and
any other I/O-layer error that doesn't have a dedicated
typed family yet. For network-specific errors, prefer
:class:`flare.net.NetworkError` and friends.

Carries:

- ``op`` — the operation being performed at the time of
  failure (``"open"``, ``"read"``, ``"alloc"``, ``"unlink"``,
  ...). Matches the strings in libc's syscall manpages so
  the message is greppable.
- ``code`` — the OS errno value, or 0 if not applicable.
- ``detail`` — human-readable context (the path, the
  requested size, the FFI return code, ...).

Example:
```mojo
from flare.errors import IoError

def read_file(path: String) raises IoError -> List[UInt8]:
    ...
    raise IoError(
        op=String("open"),
        code=2,  # ENOENT
        detail=path,
    )
```

Implements: `Copyable`, `Movable`, `Writable`

Fields:

- `op: String`
- `code: Int`
- `detail: String`

#### `write_to`

```mojo
def write_to[W: Writer](self, mut writer: W)
```

Write ``IoError(op): detail (errno=N)`` to ``writer``.

## Functions

### `http_reason_phrase`

```mojo
def http_reason_phrase(status: Int) -> String
```

Canonical RFC 9110 reason phrase for the common status codes.

Falls back to a generic class phrase ("Client Error" / "Server
Error") for codes without a dedicated entry so the body is never
empty.

### `parse_status_error`

```mojo
def parse_status_error(error_str: String) -> Optional[MappedHandlerError]
```

Decode the :struct:`HttpStatusError` wire format.

The single authoritative parser for the format
:meth:`HttpStatusError.write_to` produces -- ``None`` when
``error_str`` is not a well-formed status-error rendering (wrong
prefix, non-numeric or out-of-range status). Kept as its own
function so producer + consumer are one defined codec pair (pinned
by the round-trip test) rather than a sniff duplicated at call
sites.

### `map_handler_error`

```mojo
def map_handler_error(error_str: String, expose: Bool) -> MappedHandlerError
```

Map an uncaught handler error's rendered string to (status, reason).

Mojo's ``Handler.serve`` is bare ``raises``, which erases the
concrete error type at the catch site (no typed downcast). The
typed error's ``Writable`` rendering is the transport; we recover
intent from it:

- ``HttpStatusError(<n>): <msg>`` -> ``(n, msg)`` via the defined
  :func:`parse_status_error` codec (message always echoed -- the
  handler authored it on purpose).
- ``ValidationError(...)`` -> ``(400, "Bad Request")`` (or the raw
  string when ``expose``).
- anything else -> ``(500, "Internal Server Error")`` (or the raw
  string when ``expose``).

### `format_error_log`

```mojo
def format_error_log(kind: String, request_id: String, message: String) -> String
```

Render the log line for a sanitised error response.

Format: ``[flare:<kind>] rid=<request id or -> <full message>``. The
request id is the inbound ``X-Request-Id`` header (``-`` when the
request carried none). Control bytes in the id and the message are
escaped, so the line is always one line.

### `log_error_response`

```mojo
def log_error_response(kind: String, request_id: String, message: String)
```

Log the full message of an error whose response body is sanitised.

Writes :func:`format_error_log` to stderr. This is the server-side
half of the sanitised-error policy (``docs/security.md``): the client
gets a fixed status reason, the operator gets the message and the
request id that ties it to a request.

### `log_handler_error`

```mojo
def log_handler_error(request_id: String, error_str: String)
```

Log an uncaught handler error, as ``[flare:handler-error]``.

Skips :struct:`HttpStatusError`: its message was written by the
handler for the client and is sent as is, so there is nothing hidden
to log. Every other error (a plain ``Error``, ``ValidationError``)
maps to a fixed status reason unless ``expose_error_messages`` is on,
and is logged in full either way.

## Aliases

### `HTTP_STATUS_ERROR_PREFIX`

```mojo
comptime HTTP_STATUS_ERROR_PREFIX = String("HttpStatusError(")
```

Shared prefix for the :struct:`HttpStatusError` wire format, used by both the renderer (:meth:`HttpStatusError.write_to`) and the parser (:func:`parse_status_error`) so the codec cannot drift.
