Errors
One typed error per service, the same on every wire: #[architect::error], ClientError, the HTTP envelope.
What it is and when to use it#
architect keeps two kinds of failure apart: the service’s own typed error,
which travels the wire as a value, and infrastructure failures (no
connection, a dropped link, a bad body). #[architect::error] on an enum
writes its wire derives, an HttpError impl (status and code per variant), a
Transport(String) variant and a From<architect::TransportError>. Clients
fail with ClientError<E> (App, Connect, Transport), and
vox_into_app / ClientError::into_app fold infrastructure failures back
into E, so a caller matches one enum.
Write one error enum per service. On HTTP an error is
{code, message, error} with the variant’s status. Design:
Errors.
A minimal example#
From examples/greeter/src/lib.rs. The crate needs thiserror as a dependency.
#[architect::error]
#[derive(Eq)]
pub enum GreetError {
#[error("nobody is called {0}")]
#[architect(http_status = 404)] // also: code = "...", transport
Unknown(String),
}
That adds GreetError::Transport(String) (503, code transport). Over HTTP
a miss answers:
404 {"code":"unknown","message":"nobody is called x","error":{"Unknown":"x"}}
A client of a vox-only or sync trait folds the vox error itself:
let hello = client.greet(name).await.map_err(architect::vox_into_app)?;
Without http_status, a variant’s status comes from its name: NotFound
404; Unauthenticated, InvalidCredentials, SessionExpired 401;
Forbidden, PermissionDenied 403; Conflict 409; Internal 500;
anything else 400. Errors raised before the method runs are
422 invalid_body, 400 invalid_path and 400 invalid_query.
Code tour#
crates/architect-derive/src/lib.rserror,expand_error,default_status: the derives, theHttpErrorimpl, the transport variant.crates/architect/src/lib.rsTransportError(detail,retryable) andRepoError, the entity repos’ error.crates/architect/src/lib.rsClientError(app,is_infrastructure,is_retryable,into_app) andvox_into_app.crates/architect/src/http.rsHttpError,ErrorBody: the trait the envelope is built from, and the envelope.crates/architect/src/http.rsrespond,err: the server side.crates/architect/src/http.rsHttpClient: a non-2xx reply decodes asErrorBody<E>intoClientError::App; anything else isTransport, retryable on 5xx.
The handler returns Err(GreetError::Unknown(..)). On HTTP, err takes the
status from the HttpError impl and writes ErrorBody { code, message, error };
the HTTP client decodes App(e) and the generated trait impl returns e. On
vox, the error arrives as the call’s user error and vox_into_app returns
it; a dropped connection becomes GreetError::Transport(..), with vox’s
judgment of whether a retry can help.
Tests to read#
crates/architect/tests/http_routes.rs: a#[architect::error]enum, theinvalid_pathandinvalid_querycodes, a typedNotFoundon every face.crates/architect/tests/http_bytes.rs: the typed error as a 404 on a byte route.crates/architect/tests/http_face.rs:"code":"unknown"and422 invalid_bodyfrom a hand-writtenHttpError.examples/greeter/tests/round_trip.rs: the sameGreetError::Unknownon every wire.
Status#
Done in architect. One gap: an action projected from a service
(#[architect::service(actions)]) fails with the error’s Display, not the
HTTP face’s {code, message}.
- Task does not use
#[architect::error]orvox_into_appyet; it has its own client error types (for exampletask:features/files/files-client/src/lib.rs). - Studio does not use them yet.