Skip to main content

systemprompt_api/error/
mod.rs

1//! Entry-local HTTP error type for non-OAuth API routes.
2//!
3//! Handlers return `Result<_, ApiHttpError>` and propagate domain, repository,
4//! and service errors with `?`. The variant-to-HTTP-status mapping lives once,
5//! in the `conversions` submodule's `From` impls, so `domain/*` never
6//! references the HTTP envelope and the boundary decides the status code from
7//! the error variant rather than at each call site.
8//!
9//! The wire shape is the shared [`ApiError`] JSON envelope; [`ApiHttpError`] is
10//! a thin entry-local newtype whose only reason to exist is the orphan rule —
11//! `impl From<DomainError> for ApiError` is forbidden in this crate (both types
12//! are foreign), so a local target type is required to obtain bare `?`.
13//! `into_response` delegates to `ApiError`, which logs exactly once by status
14//! class.
15//!
16//! A 5xx never carries the underlying error's text: [`ApiError`] serialises the
17//! fixed public message of its code for every 5xx and logs the attached
18//! source chain once, so 5xx paths are built with `ApiError::internal(ctx, e)`.
19//!
20//! Copyright (c) systemprompt.io — Business Source License 1.1.
21//! See <https://systemprompt.io> for licensing details.
22
23mod conversions;
24
25use axum::response::{IntoResponse, Response};
26use systemprompt_models::api::ApiError;
27
28#[derive(Debug)]
29pub struct ApiHttpError(ApiError);
30
31impl ApiHttpError {
32    pub fn not_found(message: impl Into<String>) -> Self {
33        Self(ApiError::not_found(message))
34    }
35
36    pub fn bad_request(message: impl Into<String>) -> Self {
37        Self(ApiError::bad_request(message))
38    }
39
40    pub fn unauthorized(message: impl Into<String>) -> Self {
41        Self(ApiError::unauthorized(message))
42    }
43
44    pub fn forbidden(message: impl Into<String>) -> Self {
45        Self(ApiError::forbidden(message))
46    }
47
48    pub fn internal_error(context: &'static str) -> Self {
49        Self(ApiError::internal_error(context))
50    }
51
52    pub fn conflict(message: impl Into<String>) -> Self {
53        Self(ApiError::conflict(message))
54    }
55
56    pub fn into_inner(self) -> ApiError {
57        self.0
58    }
59}
60
61impl From<ApiError> for ApiHttpError {
62    fn from(error: ApiError) -> Self {
63        Self(error)
64    }
65}
66
67impl IntoResponse for ApiHttpError {
68    fn into_response(self) -> Response {
69        self.0.into_response()
70    }
71}