Skip to content

brokkr-broker::api::v1::error

Rust

Canonical error model for the v1 API.

Every fallible v1 handler returns Result<T, ApiError>. ApiError carries an HTTP status, a stable machine-readable code, a human-readable message, and an optional structured details map. It serializes to the ErrorResponse wire format, which is documented in the OpenAPI spec and consumed by generated SDK clients.

The code strings are part of the SDK contract — callers pattern-match on them — so they should be chosen conservatively and treated as breaking when renamed.

pub

Derives: Debug, Clone, Serialize, ToSchema

Wire format for every 4xx/5xx response body in the v1 API.

Name Type Description
code String Stable, machine-readable error code (e.g. agent_not_found).
message String Human-readable message; not stable and may change between versions.
details Option<BTreeMap<String, Value>> Optional structured context. Keys and value shapes are documented per error code when present.

pub

Derives: Debug, Clone

Errors returned by v1 handlers. Maps 1:1 to an HTTP status and an ErrorResponse body via IntoResponse.

Name Type Description
status StatusCode
code String
message String
details Option<BTreeMap<String, Value>>

pub

fn with_details(mut self, details: BTreeMap<String, Value>) -> Self

Attach structured context to an error. Useful for surfacing the ID that wasn’t found, the field that failed validation, etc.

Source
pub fn with_details(mut self, details: BTreeMap<String, Value>) -> Self {
self.details = Some(details);
self
}

pub

fn bad_request(
code: impl Into<String>,
message: impl Into<String>,
) -> Self
Source
pub fn bad_request(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, code, message)
}

pub

fn unauthorized(
code: impl Into<String>,
message: impl Into<String>,
) -> Self
Source
pub fn unauthorized(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::UNAUTHORIZED, code, message)
}

pub

fn forbidden(
code: impl Into<String>,
message: impl Into<String>,
) -> Self
Source
pub fn forbidden(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::FORBIDDEN, code, message)
}

pub

fn not_found(
code: impl Into<String>,
message: impl Into<String>,
) -> Self
Source
pub fn not_found(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::NOT_FOUND, code, message)
}

pub

fn conflict(code: impl Into<String>, message: impl Into<String>) -> Self
Source
pub fn conflict(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::CONFLICT, code, message)
}

pub

fn unprocessable(
code: impl Into<String>,
message: impl Into<String>,
) -> Self
Source
pub fn unprocessable(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::UNPROCESSABLE_ENTITY, code, message)
}

pub

fn internal(message: impl Into<String>) -> Self
Source
pub fn internal(message: impl Into<String>) -> Self {
Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", message)
}

pub

fn service_unavailable(
code: impl Into<String>,
message: impl Into<String>,
) -> Self
Source
pub fn service_unavailable(code: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(StatusCode::SERVICE_UNAVAILABLE, code, message)
}

pub

fn from_diesel(
err: diesel::result::Error,
internal_message: impl Into<String>,
) -> Self
Source
pub fn from_diesel(err: diesel::result::Error, internal_message: impl Into<String>) -> Self {
use diesel::result::{DatabaseErrorKind, Error as DieselErr};
let internal_message = internal_message.into();
match err {
DieselErr::DatabaseError(DatabaseErrorKind::UniqueViolation, info) => {
let constraint = info.constraint_name().unwrap_or("unique constraint");
let mut details = std::collections::BTreeMap::new();
details.insert(
"constraint".into(),
serde_json::Value::String(constraint.to_string()),
);
ApiError::conflict(
"unique_violation",
format!("violates unique constraint `{constraint}`"),
)
.with_details(details)
}
DieselErr::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, info) => {
let constraint = info.constraint_name().unwrap_or("foreign-key constraint");
let mut details = std::collections::BTreeMap::new();
details.insert(
"constraint".into(),
serde_json::Value::String(constraint.to_string()),
);
ApiError::unprocessable(
"foreign_key_violation",
format!("violates foreign-key constraint `{constraint}`"),
)
.with_details(details)
}
DieselErr::DatabaseError(DatabaseErrorKind::CheckViolation, info) => {
let constraint = info.constraint_name().unwrap_or("check constraint");
ApiError::unprocessable(
"check_violation",
format!("violates check constraint `{constraint}`"),
)
}
DieselErr::DatabaseError(DatabaseErrorKind::NotNullViolation, info) => {
let column = info.column_name().unwrap_or("required column");
ApiError::unprocessable(
"not_null_violation",
format!("required column `{column}` is null"),
)
}
DieselErr::NotFound => ApiError::not_found("not_found", internal_message),
other => {
tracing::error!("diesel error: {:?}", other);
ApiError::internal(internal_message)
}
}
}