From 27d560714b59b51373da783636d72b8c279ad51e Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Sat, 25 May 2024 17:27:56 -0700 Subject: [PATCH] Add custom error type using `thiserror` - Add a custom error type using `thiserror` and replace uses of `anyhow` throughout the code with the error. - Add a type alias (`RoadsterResult`) to make it easy to use our custom error type. - Add an `HttpError` type to help with creating errors with http status codes - Implement traits to allow returning the errors as responses in HTTP APIs defined with axum and/or aide Closes https://github.com/roadster-rs/roadster/issues/8 --- Cargo.toml | 1 + examples/minimal/src/app.rs | 5 +- examples/minimal/src/cli/mod.rs | 5 +- examples/minimal/src/controller/example.rs | 6 +- examples/minimal/src/main.rs | 3 +- foo | 0 src/{controller => api}/http/docs.rs | 10 +- src/{controller => api}/http/health.rs | 14 +- src/{controller => api}/http/mod.rs | 0 src/{controller => api}/http/ping.rs | 6 +- src/{controller => api}/mod.rs | 0 src/app.rs | 19 +- src/app_context.rs | 5 +- src/cli/mod.rs | 9 +- src/cli/roadster/migrate.rs | 9 +- src/cli/roadster/mod.rs | 11 +- src/cli/roadster/print_config.rs | 3 +- src/config/app_config.rs | 11 +- src/config/environment.rs | 3 +- src/error/api/http.rs | 227 ++++++++++++++++++ src/error/api/mod.rs | 37 +++ src/error/auth.rs | 29 +++ src/error/axum.rs | 27 +++ src/error/config.rs | 16 ++ src/error/mod.rs | 92 +++++++ src/error/other.rs | 22 ++ src/error/serde.rs | 34 +++ src/error/sidekiq.rs | 34 +++ src/error/tokio.rs | 16 ++ src/error/tracing.rs | 72 ++++++ src/lib.rs | 6 +- src/middleware/http/auth/jwt/ietf.rs | 5 +- src/middleware/http/auth/jwt/mod.rs | 6 +- src/service/http/builder.rs | 21 +- src/service/http/initializer/mod.rs | 9 +- .../http/initializer/normalize_path.rs | 3 +- src/service/http/middleware/catch_panic.rs | 3 +- src/service/http/middleware/compression.rs | 5 +- src/service/http/middleware/mod.rs | 3 +- src/service/http/middleware/request_id.rs | 5 +- .../http/middleware/sensitive_headers.rs | 7 +- src/service/http/middleware/size_limit.rs | 7 +- src/service/http/middleware/timeout.rs | 3 +- src/service/http/middleware/tracing.rs | 3 +- src/service/http/service.rs | 7 +- src/service/mod.rs | 10 +- src/service/registry.rs | 11 +- src/service/runner.rs | 15 +- src/service/worker/sidekiq/app_worker.rs | 3 +- src/service/worker/sidekiq/builder.rs | 35 +-- src/service/worker/sidekiq/mod.rs | 5 +- src/service/worker/sidekiq/service.rs | 5 +- src/tracing/mod.rs | 3 +- src/view/http/app_error.rs | 75 ------ src/view/http/mod.rs | 1 - src/view/mod.rs | 2 - 56 files changed, 774 insertions(+), 210 deletions(-) delete mode 100644 foo rename src/{controller => api}/http/docs.rs (95%) rename src/{controller => api}/http/health.rs (96%) rename src/{controller => api}/http/mod.rs (100%) rename src/{controller => api}/http/ping.rs (95%) rename src/{controller => api}/mod.rs (100%) create mode 100644 src/error/api/http.rs create mode 100644 src/error/api/mod.rs create mode 100644 src/error/auth.rs create mode 100644 src/error/axum.rs create mode 100644 src/error/config.rs create mode 100644 src/error/mod.rs create mode 100644 src/error/other.rs create mode 100644 src/error/serde.rs create mode 100644 src/error/sidekiq.rs create mode 100644 src/error/tokio.rs create mode 100644 src/error/tracing.rs delete mode 100644 src/view/http/app_error.rs delete mode 100644 src/view/http/mod.rs delete mode 100644 src/view/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 47fc03cd..de772186 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ num-traits = "0.2.19" log = "0.4.21" futures = "0.3.30" validator = { version = "0.18.1", features = ["derive"] } +thiserror = "1.0.61" [dev-dependencies] cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] } diff --git a/examples/minimal/src/app.rs b/examples/minimal/src/app.rs index 2673ce26..57dd64c5 100644 --- a/examples/minimal/src/app.rs +++ b/examples/minimal/src/app.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use migration::Migrator; use roadster::app::App as RoadsterApp; use roadster::app_context::AppContext; +use roadster::error::RoadsterResult; use roadster::service::http::service::HttpService; use roadster::service::registry::ServiceRegistry; use roadster::service::worker::sidekiq::app_worker::AppWorker; @@ -23,14 +24,14 @@ impl RoadsterApp for App { type Cli = AppCli; type M = Migrator; - async fn with_state(_context: &AppContext) -> anyhow::Result { + async fn with_state(_context: &AppContext) -> RoadsterResult { Ok(()) } async fn services( registry: &mut ServiceRegistry, context: &AppContext, - ) -> anyhow::Result<()> { + ) -> RoadsterResult<()> { registry .register_builder( HttpService::builder(Some(BASE), context).api_router(controller::routes(BASE)), diff --git a/examples/minimal/src/cli/mod.rs b/examples/minimal/src/cli/mod.rs index 103c4a76..e370c492 100644 --- a/examples/minimal/src/cli/mod.rs +++ b/examples/minimal/src/cli/mod.rs @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand}; use roadster::app_context::AppContext; use roadster::cli::RunCommand; +use roadster::error::RoadsterResult; use crate::app::App; use crate::app_state::CustomAppContext; @@ -24,7 +25,7 @@ impl RunCommand for AppCli { app: &App, cli: &AppCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { if let Some(command) = self.command.as_ref() { command.run(app, cli, context).await } else { @@ -46,7 +47,7 @@ impl RunCommand for AppCommand { _app: &App, _cli: &AppCli, _context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { Ok(false) } } diff --git a/examples/minimal/src/controller/example.rs b/examples/minimal/src/controller/example.rs index 4f4aaf77..c976eedd 100644 --- a/examples/minimal/src/controller/example.rs +++ b/examples/minimal/src/controller/example.rs @@ -5,9 +5,9 @@ use aide::axum::ApiRouter; use aide::transform::TransformOperation; use axum::extract::State; use axum::Json; -use roadster::controller::http::build_path; +use roadster::api::http::build_path; +use roadster::error::RoadsterResult; use roadster::service::worker::sidekiq::app_worker::AppWorker; -use roadster::view::http::app_error::AppError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use tracing::instrument; @@ -26,7 +26,7 @@ pub fn routes(parent: &str) -> ApiRouter { pub struct ExampleResponse {} #[instrument(skip_all)] -async fn example_get(State(state): State) -> Result, AppError> { +async fn example_get(State(state): State) -> RoadsterResult> { ExampleWorker::enqueue(&state, "Example".to_string()).await?; Ok(Json(ExampleResponse {})) } diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index 395515e7..a6c56bea 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -1,8 +1,9 @@ use minimal::app::App; use roadster::app; +use roadster::error::RoadsterResult; #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> RoadsterResult<()> { app::run(App).await?; Ok(()) diff --git a/foo b/foo deleted file mode 100644 index e69de29b..00000000 diff --git a/src/controller/http/docs.rs b/src/api/http/docs.rs similarity index 95% rename from src/controller/http/docs.rs rename to src/api/http/docs.rs index 9f620bdb..fe509e89 100644 --- a/src/controller/http/docs.rs +++ b/src/api/http/docs.rs @@ -1,5 +1,5 @@ +use crate::api::http::build_path; use crate::app_context::AppContext; -use crate::controller::http::build_path; use aide::axum::routing::get_with; use aide::axum::{ApiRouter, IntoApiResponse}; use aide::openapi::OpenApi; @@ -10,7 +10,6 @@ use axum::{Extension, Json}; use std::ops::Deref; use std::sync::Arc; -const BASE: &str = "_docs"; const TAG: &str = "Docs"; /// This API is only available when using Aide. @@ -18,8 +17,7 @@ pub fn routes(parent: &str, context: &AppContext) -> ApiRouter( #[cfg(any(feature = "sidekiq", feature = "db-sql"))] State(state): State>, -) -> Result, AppError> +) -> RoadsterResult> where S: Clone + Send + Sync + 'static, { @@ -167,7 +167,7 @@ where #[cfg(feature = "db-sql")] #[instrument(skip_all)] -async fn ping_db(db: &DatabaseConnection) -> anyhow::Result<()> { +async fn ping_db(db: &DatabaseConnection) -> RoadsterResult<()> { db.ping().await?; Ok(()) } @@ -191,7 +191,7 @@ async fn redis_health(redis: &sidekiq::RedisPool) -> ResourceHealth { #[cfg(feature = "sidekiq")] #[instrument(skip_all)] -async fn ping_redis(redis: &sidekiq::RedisPool) -> anyhow::Result<(Duration, Duration)> { +async fn ping_redis(redis: &sidekiq::RedisPool) -> RoadsterResult<(Duration, Duration)> { let timer = Instant::now(); let mut conn = timeout(Duration::from_secs(1), redis.get()).await??; let acquire_conn_latency = timer.elapsed(); @@ -207,7 +207,7 @@ async fn ping_redis(redis: &sidekiq::RedisPool) -> anyhow::Result<(Duration, Dur if pong == msg { Ok((acquire_conn_latency, ping_latency)) } else { - bail!("Ping response does not match input.") + Err(anyhow!("Ping response does not match input.").into()) } } diff --git a/src/controller/http/mod.rs b/src/api/http/mod.rs similarity index 100% rename from src/controller/http/mod.rs rename to src/api/http/mod.rs diff --git a/src/controller/http/ping.rs b/src/api/http/ping.rs similarity index 95% rename from src/controller/http/ping.rs rename to src/api/http/ping.rs index 5677e82f..ba27b2e7 100644 --- a/src/controller/http/ping.rs +++ b/src/api/http/ping.rs @@ -1,6 +1,6 @@ +use crate::api::http::build_path; use crate::app_context::AppContext; -use crate::controller::http::build_path; -use crate::view::http::app_error::AppError; +use crate::error::RoadsterResult; #[cfg(feature = "open-api")] use aide::axum::routing::get_with; #[cfg(feature = "open-api")] @@ -71,7 +71,7 @@ fn route(context: &AppContext) -> &str { pub struct PingResponse {} #[instrument(skip_all)] -async fn ping_get() -> Result, AppError> { +async fn ping_get() -> RoadsterResult> { Ok(Json(PingResponse::default())) } diff --git a/src/controller/mod.rs b/src/api/mod.rs similarity index 100% rename from src/controller/mod.rs rename to src/api/mod.rs diff --git a/src/app.rs b/src/app.rs index 93b1f151..d98434dc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use crate::cli::RunCommand; use crate::config::app_config::AppConfig; #[cfg(not(feature = "cli"))] use crate::config::environment::Environment; +use crate::error::RoadsterResult; use crate::service::registry::ServiceRegistry; use crate::tracing::init_tracing; use async_trait::async_trait; @@ -26,7 +27,7 @@ use tracing::{instrument, warn}; pub async fn run( // This parameter is (currently) not used when no features are enabled. #[allow(unused_variables)] app: A, -) -> anyhow::Result<()> +) -> RoadsterResult<()> where A: App + Default + Send + Sync + 'static, { @@ -47,10 +48,12 @@ where #[cfg(feature = "cli")] config.validate(!roadster_cli.skip_validate_config)?; + // The `config.clone()` here is technically not necessary. However, without it, RustRover + // is giving a "value used after move" error when creating an actual `AppContext` below. + #[cfg(test)] + let context = AppContext::<()>::test(Some(config.clone()), None)?; #[cfg(not(test))] let context = AppContext::<()>::new::(config).await?; - #[cfg(test)] - let context = AppContext::<()>::test(Some(config), None)?; let state = A::with_state(&context).await?; let context = context.with_custom(state); @@ -95,14 +98,14 @@ pub trait App: Send + Sync { #[cfg(feature = "db-sql")] type M: MigratorTrait; - fn init_tracing(config: &AppConfig) -> anyhow::Result<()> { + fn init_tracing(config: &AppConfig) -> RoadsterResult<()> { init_tracing(config)?; Ok(()) } #[cfg(feature = "db-sql")] - fn db_connection_options(config: &AppConfig) -> anyhow::Result { + fn db_connection_options(config: &AppConfig) -> RoadsterResult { let mut options = ConnectOptions::new(config.database.uri.to_string()); options .connect_timeout(config.database.connect_timeout) @@ -124,13 +127,13 @@ pub trait App: Send + Sync { /// method is provided in case there's any additional work that needs to be done that the /// consumer can't put in a [`From`] implementation. For example, any /// configuration that needs to happen in an async method. - async fn with_state(context: &AppContext<()>) -> anyhow::Result; + async fn with_state(context: &AppContext<()>) -> RoadsterResult; /// Provide the services to run in the app. async fn services( _registry: &mut ServiceRegistry, _context: &AppContext, - ) -> anyhow::Result<()> { + ) -> RoadsterResult<()> { Ok(()) } @@ -144,7 +147,7 @@ pub trait App: Send + Sync { /// Override to provide custom graceful shutdown logic to clean up any resources created by /// the app. Roadster will take care of cleaning up the resources it created. #[instrument(skip_all)] - async fn graceful_shutdown(_context: &AppContext) -> anyhow::Result<()> { + async fn graceful_shutdown(_context: &AppContext) -> RoadsterResult<()> { Ok(()) } } diff --git a/src/app_context.rs b/src/app_context.rs index 6a49ab27..d262d21e 100644 --- a/src/app_context.rs +++ b/src/app_context.rs @@ -1,5 +1,6 @@ use crate::app::App; use crate::config::app_config::AppConfig; +use crate::error::RoadsterResult; #[cfg(feature = "db-sql")] use sea_orm::DatabaseConnection; use std::sync::Arc; @@ -20,7 +21,7 @@ impl AppContext { #[cfg_attr(test, allow(dead_code))] // The `A` type parameter isn't used in some feature configurations #[allow(clippy::extra_unused_type_parameters)] - pub(crate) async fn new(config: AppConfig) -> anyhow::Result> + pub(crate) async fn new(config: AppConfig) -> RoadsterResult> where A: App, { @@ -90,7 +91,7 @@ impl AppContext { config: Option, #[cfg(not(feature = "sidekiq"))] _redis: Option<()>, #[cfg(feature = "sidekiq")] redis: Option, - ) -> anyhow::Result> { + ) -> RoadsterResult> { let mut inner = MockAppContextInner::default(); inner .expect_config() diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6e5616fb..7ba2378c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,6 +3,7 @@ use crate::app::App; use crate::app::MockApp; use crate::app_context::AppContext; use crate::cli::roadster::{RoadsterCli, RunRoadsterCommand}; +use crate::error::RoadsterResult; use async_trait::async_trait; use clap::{Args, Command, FromArgMatches}; use std::ffi::OsString; @@ -29,10 +30,10 @@ where app: &A, cli: &A::Cli, context: &AppContext, - ) -> anyhow::Result; + ) -> RoadsterResult; } -pub(crate) fn parse_cli(args: I) -> anyhow::Result<(RoadsterCli, A::Cli)> +pub(crate) fn parse_cli(args: I) -> RoadsterResult<(RoadsterCli, A::Cli)> where A: App, I: IntoIterator, @@ -82,7 +83,7 @@ pub(crate) async fn handle_cli( roadster_cli: &RoadsterCli, app_cli: &A::Cli, context: &AppContext, -) -> anyhow::Result +) -> RoadsterResult where A: App, { @@ -106,7 +107,7 @@ mockall::mock! { app: &MockApp, cli: &::Cli, context: &AppContext<::State>, - ) -> anyhow::Result; + ) -> RoadsterResult; } impl clap::FromArgMatches for Cli { diff --git a/src/cli/roadster/migrate.rs b/src/cli/roadster/migrate.rs index 659a32b6..a4d2bca3 100644 --- a/src/cli/roadster/migrate.rs +++ b/src/cli/roadster/migrate.rs @@ -1,4 +1,4 @@ -use anyhow::bail; +use anyhow::anyhow; use async_trait::async_trait; use clap::{Parser, Subcommand}; use sea_orm_migration::MigratorTrait; @@ -8,6 +8,7 @@ use tracing::warn; use crate::app::App; use crate::app_context::AppContext; use crate::cli::roadster::{RoadsterCli, RunRoadsterCommand}; +use crate::error::RoadsterResult; #[derive(Debug, Parser, Serialize)] pub struct MigrateArgs { @@ -25,7 +26,7 @@ where app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { self.command.run(app, cli, context).await } } @@ -57,9 +58,9 @@ where _app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { if is_destructive(self) && !cli.allow_dangerous(context) { - bail!("Running destructive command `{:?}` is not allowed in environment `{:?}`. To override, provide the `--allow-dangerous` CLI arg.", self, context.config().environment); + return Err(anyhow!("Running destructive command `{:?}` is not allowed in environment `{:?}`. To override, provide the `--allow-dangerous` CLI arg.", self, context.config().environment).into()); } else if is_destructive(self) { warn!( "Running destructive command `{:?}` in environment `{:?}`", diff --git a/src/cli/roadster/mod.rs b/src/cli/roadster/mod.rs index f163936e..cea01947 100644 --- a/src/cli/roadster/mod.rs +++ b/src/cli/roadster/mod.rs @@ -8,6 +8,7 @@ use crate::cli::roadster::migrate::MigrateArgs; use crate::cli::roadster::open_api_schema::OpenApiArgs; use crate::cli::roadster::print_config::PrintConfigArgs; use crate::config::environment::Environment; +use crate::error::RoadsterResult; use async_trait::async_trait; use clap::{Parser, Subcommand}; use serde_derive::Serialize; @@ -33,7 +34,7 @@ where app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result; + ) -> RoadsterResult; } /// Roadster: The Roadster CLI provides various utilities for managing your application. If no subcommand @@ -77,7 +78,7 @@ where app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { if let Some(command) = self.command.as_ref() { command.run(app, cli, context).await } else { @@ -105,7 +106,7 @@ where app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { match self { RoadsterCommand::Roadster(args) => args.run(app, cli, context).await, } @@ -128,7 +129,7 @@ where app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { self.command.run(app, cli, context).await } } @@ -143,7 +144,7 @@ where app: &A, cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { match self { #[cfg(feature = "open-api")] RoadsterSubCommand::ListRoutes(_) => { diff --git a/src/cli/roadster/print_config.rs b/src/cli/roadster/print_config.rs index 5fc5177e..606a2898 100644 --- a/src/cli/roadster/print_config.rs +++ b/src/cli/roadster/print_config.rs @@ -7,6 +7,7 @@ use tracing::info; use crate::app::App; use crate::app_context::AppContext; use crate::cli::roadster::{RoadsterCli, RunRoadsterCommand}; +use crate::error::RoadsterResult; #[derive(Debug, Parser, Serialize)] pub struct PrintConfigArgs { @@ -38,7 +39,7 @@ where _app: &A, _cli: &RoadsterCli, context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { match self.format { Format::Debug => { info!("\n{:?}", context.config()) diff --git a/src/config/app_config.rs b/src/config/app_config.rs index 6fd20959..f90a074a 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -4,8 +4,8 @@ use crate::config::database::Database; use crate::config::environment::{Environment, ENVIRONMENT_ENV_VAR_NAME}; use crate::config::service::Service; use crate::config::tracing::Tracing; +use crate::error::RoadsterResult; use crate::util::serde_util::default_true; -use anyhow::anyhow; use config::{Case, Config}; use dotenvy::dotenv; use serde_derive::{Deserialize, Serialize}; @@ -60,7 +60,7 @@ impl AppConfig { // This runs before tracing is initialized, so we need to use `println` in order to // log from this method. #[allow(clippy::disallowed_macros)] - pub fn new(environment: Option) -> anyhow::Result { + pub fn new(environment: Option) -> RoadsterResult { dotenv().ok(); let environment = if let Some(environment) = environment { @@ -86,14 +86,13 @@ impl AppConfig { ) .set_override(ENVIRONMENT_ENV_VAR_NAME, environment_str)? .build()? - .try_deserialize() - .map_err(|err| anyhow!("Unable to deserialize app config: {err:?}"))?; + .try_deserialize()?; Ok(config) } #[cfg(test)] - pub(crate) fn test(config_str: Option<&str>) -> anyhow::Result { + pub(crate) fn test(config_str: Option<&str>) -> RoadsterResult { let config = config_str.unwrap_or( r#" environment = "test" @@ -125,7 +124,7 @@ impl AppConfig { Ok(config) } - pub(crate) fn validate(&self, exit_on_error: bool) -> anyhow::Result<()> { + pub(crate) fn validate(&self, exit_on_error: bool) -> RoadsterResult<()> { let result = Validate::validate(self); if exit_on_error { result?; diff --git a/src/config/environment.rs b/src/config/environment.rs index 73633d4d..78e7f00e 100644 --- a/src/config/environment.rs +++ b/src/config/environment.rs @@ -9,6 +9,7 @@ use serde_derive::{Deserialize, Serialize}; use strum_macros::{EnumString, IntoStaticStr}; use crate::config::app_config::{ENV_VAR_PREFIX, ENV_VAR_SEPARATOR}; +use crate::error::RoadsterResult; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, EnumString, IntoStaticStr)] #[cfg_attr(feature = "cli", derive(ValueEnum))] @@ -29,7 +30,7 @@ impl Environment { // This runs before tracing is initialized, so we need to use `println` in order to // log from this method. #[allow(clippy::disallowed_macros)] - pub fn new() -> anyhow::Result { + pub fn new() -> RoadsterResult { // Get the stage, and validate it by parsing to the Environment enum let environment = env::var(ENV_VAR_WITH_PREFIX) .map_err(|_| anyhow!("Env var `{ENV_VAR_WITH_PREFIX}` not defined."))?; diff --git a/src/error/api/http.rs b/src/error/api/http.rs new file mode 100644 index 00000000..f6f3b689 --- /dev/null +++ b/src/error/api/http.rs @@ -0,0 +1,227 @@ +use crate::error::Error; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use serde_derive::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +/// Error type representing an HTTP API error. This is generally expected to be returned explicitly +/// by your application logic. +/// +/// # Examples +/// +/// ## Alternative -- directly use `StatusCode` +/// If you simply need to create an Axum response with just a status code, this class is not +/// necessary. You can instead use `StatusCode` directly: +/// +/// ```rust +/// use axum::http::StatusCode; +/// use axum::response::IntoResponse; +/// +/// fn api_method() -> impl IntoResponse { +/// StatusCode::BAD_REQUEST +/// } +/// ``` +/// +/// This can also work when your api method returns a result, either with a generic response: +/// +/// ```rust +/// use axum::http::StatusCode; +/// use axum::response::IntoResponse; +/// +/// fn api_method() -> Result<(), impl IntoResponse> { +/// Err(StatusCode::BAD_REQUEST) +/// } +/// ``` +/// +/// Or when returning a [roadster result][crate::error::RoadsterResult] (which uses a +/// [roadster error][enum@Error] for its `Error` type). +/// +/// ```rust +/// use axum::http::StatusCode; +/// use axum::response::IntoResponse; +/// use roadster::error::RoadsterResult; +/// +/// fn api_method() -> RoadsterResult<()> { +/// Err(StatusCode::BAD_REQUEST.into()) +/// } +/// ``` +/// +/// ## Create from `StatusCode` +/// +/// ```rust +/// use axum::http::StatusCode; +/// use roadster::error::api::http::HttpError; +/// +/// let err: HttpError = StatusCode::BAD_REQUEST.into(); +/// ``` +/// +/// ## Create using a helper method +/// +/// ```rust +/// use roadster::error::api::http::HttpError; +/// +/// let err = HttpError::bad_request(); +/// ``` +/// +/// ## Populate additional fields with builder-style methods +/// +/// ```rust +/// use roadster::error::api::http::HttpError; +/// +/// let err = HttpError::bad_request() +/// .error("Something went wrong") +/// .details("Field 'A' is missing"); +/// ``` +/// +/// ## Using in an API method +/// +/// ```rust +/// use axum::response::IntoResponse; +/// use roadster::error::api::http::HttpError; +/// +/// fn api_method() -> Result<(), impl IntoResponse> { +/// let err = HttpError::bad_request() +/// .error("Something went wrong") +/// .details("Field 'A' is missing"); +/// Err(err) +/// } +/// ``` +/// +/// ## Using in an API method that returns `RoadsterResult` +/// +/// ```rust +/// use axum::response::IntoResponse; +/// use roadster::error::api::http::HttpError; +/// use roadster::error::RoadsterResult; +/// +/// fn api_method() -> RoadsterResult<()> { +/// let err = HttpError::bad_request() +/// .error("Something went wrong") +/// .details("Field 'A' is missing"); +/// Err(err.into()) +/// } +/// ``` +/// +#[derive(Debug, Error, Serialize, Deserialize)] +#[cfg_attr(feature = "open-api", derive(aide::OperationIo, schemars::JsonSchema))] +pub struct HttpError { + /// The HTTP status code for the error. + /// + /// When this error is converted to an HTTP response, this field is set as the HTTP response + /// status header and omitted from the response body/payload. + #[serde(skip)] + pub status: StatusCode, + /// Basic description of the error that occurred. + // Todo: auto-redact sensitive data + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Additional details for the error. + // Todo: auto-redact sensitive data + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, + /// The original error. This can be logged to help with debugging, but it is omitted + /// from the response body/payload to avoid exposing sensitive details from the stacktrace + /// to the user. + #[source] + #[serde(skip)] + pub source: Option>, +} + +impl Display for HttpError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Http Error {} - {:?}", self.status, self.error) + } +} + +impl HttpError { + pub fn new(status: StatusCode) -> Self { + Self { + status, + error: None, + details: None, + source: None, + } + } + + /// Utility method to convert this [HttpError] into an [enum@Error]. + pub fn to_err(self) -> Error { + self.into() + } + + pub fn error(self, error: impl ToString) -> Self { + Self { + error: Some(error.to_string()), + ..self + } + } + + pub fn details(self, details: impl ToString) -> Self { + Self { + details: Some(details.to_string()), + ..self + } + } + + pub fn source(self, source: impl std::error::Error + Send + Sync + 'static) -> Self { + Self { + source: Some(Box::new(source)), + ..self + } + } + + // Common 4xx errors + + /// Helper method to create an error with status code [StatusCode::BAD_REQUEST] + pub fn bad_request() -> Self { + Self::new(StatusCode::BAD_REQUEST) + } + + /// Helper method to create an error with status code [StatusCode::UNAUTHORIZED] + pub fn unauthorized() -> Self { + Self::new(StatusCode::UNAUTHORIZED) + } + + /// Helper method to create an error with status code [StatusCode::FORBIDDEN] + pub fn forbidden() -> Self { + Self::new(StatusCode::FORBIDDEN) + } + + /// Helper method to create an error with status code [StatusCode::NOT_FOUND] + pub fn not_found() -> Self { + Self::new(StatusCode::NOT_FOUND) + } + + /// Helper method to create an error with status code [StatusCode::GONE] + pub fn gone() -> Self { + Self::new(StatusCode::GONE) + } + + // Common 5xx errors + + /// Helper method to create an error with status code [StatusCode::INTERNAL_SERVER_ERROR] + pub fn internal_server_error() -> Self { + Self::new(StatusCode::INTERNAL_SERVER_ERROR) + } +} + +impl From for HttpError { + fn from(value: StatusCode) -> Self { + HttpError::new(value) + } +} + +impl From for Error { + fn from(value: StatusCode) -> Self { + HttpError::new(value).into() + } +} + +impl IntoResponse for HttpError { + fn into_response(self) -> Response { + let status = self.status; + let mut res = Json(self).into_response(); + *res.status_mut() = status; + res + } +} diff --git a/src/error/api/mod.rs b/src/error/api/mod.rs new file mode 100644 index 00000000..f6cc3dd9 --- /dev/null +++ b/src/error/api/mod.rs @@ -0,0 +1,37 @@ +#[cfg(feature = "http")] +pub mod http; + +#[cfg(feature = "http")] +use crate::error::api::http::HttpError; +use crate::error::Error; +#[cfg(feature = "http")] +use axum::http::StatusCode; +#[cfg(feature = "http")] +use axum::response::{IntoResponse, Response}; + +#[derive(Debug, Error)] +pub enum ApiError { + #[cfg(feature = "http")] + #[error(transparent)] + Http(HttpError), + + #[error(transparent)] + Other(#[from] Box), +} + +#[cfg(feature = "http")] +impl From for Error { + fn from(value: HttpError) -> Self { + Self::Api(ApiError::Http(value)) + } +} + +#[cfg(feature = "http")] +impl IntoResponse for ApiError { + fn into_response(self) -> Response { + match self { + ApiError::Http(err) => err.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } + } +} diff --git a/src/error/auth.rs b/src/error/auth.rs new file mode 100644 index 00000000..feaf94dc --- /dev/null +++ b/src/error/auth.rs @@ -0,0 +1,29 @@ +use crate::error::Error; +#[cfg(feature = "http")] +use axum::http::StatusCode; +#[cfg(feature = "http")] +use axum::response::{IntoResponse, Response}; + +#[derive(Debug, Error)] +pub enum AuthError { + #[cfg(feature = "jwt")] + #[error(transparent)] + Jwt(#[from] jsonwebtoken::errors::Error), + + #[error(transparent)] + Other(#[from] Box), +} + +#[cfg(feature = "jwt")] +impl From for Error { + fn from(value: jsonwebtoken::errors::Error) -> Self { + Self::Auth(AuthError::from(value)) + } +} + +#[cfg(feature = "http")] +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + StatusCode::UNAUTHORIZED.into_response() + } +} diff --git a/src/error/axum.rs b/src/error/axum.rs new file mode 100644 index 00000000..585b72b2 --- /dev/null +++ b/src/error/axum.rs @@ -0,0 +1,27 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum AxumError { + #[error(transparent)] + InvalidHeader(#[from] axum::http::header::InvalidHeaderName), + + #[cfg(feature = "jwt")] + #[error(transparent)] + TypedHeaderRejection(#[from] axum_extra::typed_header::TypedHeaderRejection), + + #[error(transparent)] + Other(#[from] Box), +} + +impl From for Error { + fn from(value: axum::http::header::InvalidHeaderName) -> Self { + Self::Axum(AxumError::from(value)) + } +} + +#[cfg(feature = "jwt")] +impl From for Error { + fn from(value: axum_extra::typed_header::TypedHeaderRejection) -> Self { + Self::Axum(AxumError::from(value)) + } +} diff --git a/src/error/config.rs b/src/error/config.rs new file mode 100644 index 00000000..01d0f6b4 --- /dev/null +++ b/src/error/config.rs @@ -0,0 +1,16 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error(transparent)] + Config(#[from] config::ConfigError), + + #[error(transparent)] + Other(#[from] Box), +} + +impl From for Error { + fn from(value: config::ConfigError) -> Self { + Self::Config(ConfigError::from(value)) + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 00000000..2038a77a --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,92 @@ +pub mod api; +pub mod auth; +#[cfg(feature = "http")] +pub mod axum; +pub mod config; +pub mod other; +pub mod serde; +#[cfg(feature = "sidekiq")] +pub mod sidekiq; +pub mod tokio; +pub mod tracing; + +use crate::error::api::ApiError; +use crate::error::auth::AuthError; +#[cfg(feature = "http")] +use crate::error::axum::AxumError; +use crate::error::other::OtherError; +use crate::error::serde::SerdeError; +#[cfg(feature = "sidekiq")] +use crate::error::sidekiq::SidekiqError; +use crate::error::tokio::TokioError; +use crate::error::tracing::TracingError; +#[cfg(feature = "http")] +use ::axum::http::StatusCode; +#[cfg(feature = "http")] +use ::axum::response::{IntoResponse, Response}; +#[cfg(feature = "open-api")] +use aide::OperationOutput; +use thiserror::Error; + +pub type RoadsterResult = Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Api(#[from] ApiError), + + #[error(transparent)] + Auth(#[from] AuthError), + + #[error(transparent)] + Serde(#[from] SerdeError), + + #[cfg(feature = "db-sql")] + #[error(transparent)] + Db(#[from] sea_orm::DbErr), + + #[cfg(feature = "sidekiq")] + #[error(transparent)] + Sidekiq(#[from] SidekiqError), + + #[cfg(feature = "cli")] + #[error(transparent)] + Clap(#[from] clap::error::Error), + + #[error(transparent)] + Config(#[from] config::ConfigError), + + #[error(transparent)] + Validation(#[from] validator::ValidationErrors), + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Tokio(#[from] TokioError), + + #[error(transparent)] + Tracing(#[from] TracingError), + + #[cfg(feature = "http")] + #[error(transparent)] + Axum(#[from] AxumError), + + #[error(transparent)] + Other(#[from] OtherError), +} + +#[cfg(feature = "http")] +impl IntoResponse for Error { + fn into_response(self) -> Response { + match self { + Error::Api(err) => err.into_response(), + _ => StatusCode::INTERNAL_SERVER_ERROR.into_response(), + } + } +} + +#[cfg(feature = "open-api")] +impl OperationOutput for Error { + type Inner = api::http::HttpError; +} diff --git a/src/error/other.rs b/src/error/other.rs new file mode 100644 index 00000000..e9b0e7c9 --- /dev/null +++ b/src/error/other.rs @@ -0,0 +1,22 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum OtherError { + #[error(transparent)] + Anyhow(#[from] anyhow::Error), + + #[error(transparent)] + Other(#[from] Box), +} + +impl From for Error { + fn from(value: anyhow::Error) -> Self { + Self::Other(OtherError::from(value)) + } +} + +impl From> for Error { + fn from(value: Box) -> Self { + Self::Other(OtherError::from(value)) + } +} diff --git a/src/error/serde.rs b/src/error/serde.rs new file mode 100644 index 00000000..da936671 --- /dev/null +++ b/src/error/serde.rs @@ -0,0 +1,34 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum SerdeError { + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + TomlDeserialize(#[from] toml::de::Error), + + #[error(transparent)] + TomlSerialize(#[from] toml::ser::Error), + + #[error(transparent)] + Other(#[from] Box), +} + +impl From for Error { + fn from(value: serde_json::Error) -> Self { + Self::Serde(SerdeError::from(value)) + } +} + +impl From for Error { + fn from(value: toml::de::Error) -> Self { + Self::Serde(SerdeError::from(value)) + } +} + +impl From for Error { + fn from(value: toml::ser::Error) -> Self { + Self::Serde(SerdeError::from(value)) + } +} diff --git a/src/error/sidekiq.rs b/src/error/sidekiq.rs new file mode 100644 index 00000000..8f0327be --- /dev/null +++ b/src/error/sidekiq.rs @@ -0,0 +1,34 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum SidekiqError { + #[error(transparent)] + Sidekiq(#[from] sidekiq::Error), + + #[error(transparent)] + Redis(#[from] sidekiq::RedisError), + + #[error(transparent)] + Bb8(#[from] bb8::RunError), + + #[error(transparent)] + Other(#[from] Box), +} + +impl From for Error { + fn from(value: sidekiq::Error) -> Self { + Self::Sidekiq(SidekiqError::from(value)) + } +} + +impl From for Error { + fn from(value: sidekiq::RedisError) -> Self { + Self::Sidekiq(SidekiqError::from(value)) + } +} + +impl From> for Error { + fn from(value: bb8::RunError) -> Self { + Self::Sidekiq(SidekiqError::from(value)) + } +} diff --git a/src/error/tokio.rs b/src/error/tokio.rs new file mode 100644 index 00000000..ceded560 --- /dev/null +++ b/src/error/tokio.rs @@ -0,0 +1,16 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum TokioError { + #[error(transparent)] + Timeout(#[from] tokio::time::error::Elapsed), + + #[error(transparent)] + Other(#[from] Box), +} + +impl From for Error { + fn from(value: tokio::time::error::Elapsed) -> Self { + Self::Tokio(TokioError::from(value)) + } +} diff --git a/src/error/tracing.rs b/src/error/tracing.rs new file mode 100644 index 00000000..036887f0 --- /dev/null +++ b/src/error/tracing.rs @@ -0,0 +1,72 @@ +use crate::error::Error; + +#[derive(Debug, Error)] +pub enum TracingError { + /// An error that occurs during tracing initialization. + #[error(transparent)] + Init(#[from] TracingInitError), + + #[error(transparent)] + Other(#[from] Box), +} + +#[derive(Debug, Error)] +pub enum TracingInitError { + #[cfg(feature = "otel")] + #[error(transparent)] + OtelTrace(#[from] opentelemetry::trace::TraceError), + + #[cfg(feature = "otel")] + #[error(transparent)] + OtelMetrics(#[from] opentelemetry::metrics::MetricsError), + + #[error(transparent)] + ParseLevel(#[from] tracing::metadata::ParseLevelError), + + #[error(transparent)] + ParseFilter(#[from] tracing_subscriber::filter::ParseError), + + #[error(transparent)] + FilterFromEnv(#[from] tracing_subscriber::filter::FromEnvError), + + #[error(transparent)] + Init(#[from] tracing_subscriber::util::TryInitError), +} + +#[cfg(feature = "otel")] +impl From for Error { + fn from(value: opentelemetry::trace::TraceError) -> Self { + Self::Tracing(TracingError::from(TracingInitError::from(value))) + } +} + +#[cfg(feature = "otel")] +impl From for Error { + fn from(value: opentelemetry::metrics::MetricsError) -> Self { + Self::Tracing(TracingError::from(TracingInitError::from(value))) + } +} + +impl From for Error { + fn from(value: tracing::metadata::ParseLevelError) -> Self { + Self::Tracing(TracingError::from(TracingInitError::from(value))) + } +} + +impl From for Error { + fn from(value: tracing_subscriber::filter::ParseError) -> Self { + Self::Tracing(TracingError::from(TracingInitError::from(value))) + } +} + +impl From for Error { + fn from(value: tracing_subscriber::filter::FromEnvError) -> Self { + Self::Tracing(TracingError::from(TracingInitError::from(value))) + } +} + +impl From for Error { + fn from(value: tracing_subscriber::util::TryInitError) -> Self { + Self::Tracing(TracingError::from(TracingInitError::from(value))) + } +} diff --git a/src/lib.rs b/src/lib.rs index c5712840..84cf0e6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,15 +16,15 @@ // https://github.com/taiki-e/coverage-helper?tab=readme-ov-file#usage #![cfg_attr(all(test, coverage_nightly), feature(coverage_attribute))] +#[cfg(feature = "http")] +pub mod api; pub mod app; pub mod app_context; #[cfg(feature = "cli")] pub mod cli; pub mod config; -#[cfg(feature = "http")] -pub mod controller; +pub mod error; pub mod middleware; pub mod service; pub mod tracing; pub mod util; -pub mod view; diff --git a/src/middleware/http/auth/jwt/ietf.rs b/src/middleware/http/auth/jwt/ietf.rs index a28101b0..b2d208df 100644 --- a/src/middleware/http/auth/jwt/ietf.rs +++ b/src/middleware/http/auth/jwt/ietf.rs @@ -52,6 +52,7 @@ pub struct Claims { #[cfg(test)] mod tests { use super::*; + use crate::error::RoadsterResult; use crate::middleware::http::auth::jwt::decode_auth_token; use crate::util::serde_util::UriOrString; use chrono::{TimeDelta, Utc}; @@ -82,7 +83,7 @@ mod tests { fn decode_token_expired() { let (_, jwt) = build_token(true, None); - let decoded: anyhow::Result> = + let decoded: RoadsterResult> = decode_auth_token(&jwt, TEST_JWT_SECRET, AUDIENCE, REQUIRED_CLAIMS); assert!(decoded.is_err()); @@ -93,7 +94,7 @@ mod tests { fn decode_token_wrong_audience() { let (_, jwt) = build_token(false, Some("different-audience".to_string())); - let decoded: anyhow::Result> = + let decoded: RoadsterResult> = decode_auth_token(&jwt, TEST_JWT_SECRET, AUDIENCE, REQUIRED_CLAIMS); assert!(decoded.is_err()); diff --git a/src/middleware/http/auth/jwt/mod.rs b/src/middleware/http/auth/jwt/mod.rs index cf28a650..1b8bd718 100644 --- a/src/middleware/http/auth/jwt/mod.rs +++ b/src/middleware/http/auth/jwt/mod.rs @@ -4,12 +4,12 @@ pub mod ietf; pub mod openid; use crate::app_context::AppContext; +use crate::error::{Error, RoadsterResult}; #[cfg(feature = "jwt-ietf")] use crate::middleware::http::auth::jwt::ietf::Claims; #[cfg(all(feature = "jwt-openid", not(feature = "jwt-ietf")))] use crate::middleware::http::auth::jwt::openid::Claims; use crate::util::serde_util::{deserialize_from_str, serialize_to_str}; -use crate::view::http::app_error::AppError; #[cfg(feature = "open-api")] use aide::OperationInput; use async_trait::async_trait; @@ -53,7 +53,7 @@ where S: Send + Sync, C: for<'de> serde::Deserialize<'de>, { - type Rejection = AppError; + type Rejection = Error; async fn from_request_parts( parts: &mut Parts, @@ -79,7 +79,7 @@ fn decode_auth_token( jwt_secret: &str, audience: &[T1], required_claims: &[T2], -) -> anyhow::Result> +) -> RoadsterResult> where T1: ToString, T2: ToString, diff --git a/src/service/http/builder.rs b/src/service/http/builder.rs index 101bb81a..3cb11456 100644 --- a/src/service/http/builder.rs +++ b/src/service/http/builder.rs @@ -1,9 +1,10 @@ -use crate::app::App; -use crate::app_context::AppContext; #[cfg(feature = "open-api")] -use crate::controller::http::default_api_routes; +use crate::api::http::default_api_routes; #[cfg(not(feature = "open-api"))] -use crate::controller::http::default_routes; +use crate::api::http::default_routes; +use crate::app::App; +use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::initializer::default::default_initializers; use crate::service::http::initializer::Initializer; use crate::service::http::middleware::default::default_middleware; @@ -16,7 +17,7 @@ use aide::axum::ApiRouter; use aide::openapi::OpenApi; #[cfg(feature = "open-api")] use aide::transform::TransformOpenApi; -use anyhow::bail; +use anyhow::anyhow; use async_trait::async_trait; #[cfg(feature = "open-api")] use axum::Extension; @@ -98,7 +99,7 @@ impl HttpServiceBuilder { self } - pub fn initializer(mut self, initializer: T) -> anyhow::Result + pub fn initializer(mut self, initializer: T) -> RoadsterResult where T: Initializer + 'static, { @@ -111,12 +112,12 @@ impl HttpServiceBuilder { .insert(name.clone(), Box::new(initializer)) .is_some() { - bail!("Initializer `{name}` was already registered"); + return Err(anyhow!("Initializer `{name}` was already registered").into()); } Ok(self) } - pub fn middleware(mut self, middleware: T) -> anyhow::Result + pub fn middleware(mut self, middleware: T) -> RoadsterResult where T: Middleware + 'static, { @@ -129,7 +130,7 @@ impl HttpServiceBuilder { .insert(name.clone(), Box::new(middleware)) .is_some() { - bail!("Middleware `{name}` was already registered"); + return Err(anyhow!("Middleware `{name}` was already registered").into()); } Ok(self) } @@ -137,7 +138,7 @@ impl HttpServiceBuilder { #[async_trait] impl AppServiceBuilder for HttpServiceBuilder { - async fn build(self, context: &AppContext) -> anyhow::Result { + async fn build(self, context: &AppContext) -> RoadsterResult { let router = self.router; #[cfg(feature = "open-api")] diff --git a/src/service/http/initializer/mod.rs b/src/service/http/initializer/mod.rs index 1ced71d9..284b2593 100644 --- a/src/service/http/initializer/mod.rs +++ b/src/service/http/initializer/mod.rs @@ -2,6 +2,7 @@ pub mod default; pub mod normalize_path; use crate::app_context::AppContext; +use crate::error::RoadsterResult; use axum::Router; /// Provides hooks into various stages of the app's startup to allow initializing and installing @@ -27,7 +28,7 @@ where /// safe to set its priority as `0`. fn priority(&self, context: &AppContext) -> i32; - fn after_router(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn after_router(&self, router: Router, _context: &AppContext) -> RoadsterResult { Ok(router) } @@ -35,15 +36,15 @@ where &self, router: Router, _context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { Ok(router) } - fn after_middleware(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn after_middleware(&self, router: Router, _context: &AppContext) -> RoadsterResult { Ok(router) } - fn before_serve(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn before_serve(&self, router: Router, _context: &AppContext) -> RoadsterResult { Ok(router) } } diff --git a/src/service/http/initializer/normalize_path.rs b/src/service/http/initializer/normalize_path.rs index 77ce4bd7..a52b9dd2 100644 --- a/src/service/http/initializer/normalize_path.rs +++ b/src/service/http/initializer/normalize_path.rs @@ -1,4 +1,5 @@ use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::initializer::Initializer; use axum::Router; use serde_derive::{Deserialize, Serialize}; @@ -40,7 +41,7 @@ impl Initializer for NormalizePathInitializer { .priority } - fn before_serve(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn before_serve(&self, router: Router, _context: &AppContext) -> RoadsterResult { let router = NormalizePathLayer::trim_trailing_slash().layer(router); let router = Router::new().nest_service("/", router); Ok(router) diff --git a/src/service/http/middleware/catch_panic.rs b/src/service/http/middleware/catch_panic.rs index 6452d264..5e62d64c 100644 --- a/src/service/http/middleware/catch_panic.rs +++ b/src/service/http/middleware/catch_panic.rs @@ -1,4 +1,5 @@ use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::middleware::Middleware; use axum::Router; use serde_derive::{Deserialize, Serialize}; @@ -38,7 +39,7 @@ impl Middleware for CatchPanicMiddleware { .priority } - fn install(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, _context: &AppContext) -> RoadsterResult { let router = router.layer(CatchPanicLayer::new()); Ok(router) diff --git a/src/service/http/middleware/compression.rs b/src/service/http/middleware/compression.rs index 4a51a846..c6b73c4c 100644 --- a/src/service/http/middleware/compression.rs +++ b/src/service/http/middleware/compression.rs @@ -3,6 +3,7 @@ use crate::service::http::middleware::Middleware; use axum::Router; use serde_derive::{Deserialize, Serialize}; +use crate::error::RoadsterResult; use tower_http::compression::CompressionLayer; use tower_http::decompression::RequestDecompressionLayer; @@ -44,7 +45,7 @@ impl Middleware for ResponseCompressionMiddleware { .priority } - fn install(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, _context: &AppContext) -> RoadsterResult { let router = router.layer(CompressionLayer::new()); Ok(router) @@ -81,7 +82,7 @@ impl Middleware for RequestDecompressionMiddleware .priority } - fn install(&self, router: Router, _context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, _context: &AppContext) -> RoadsterResult { let router = router.layer(RequestDecompressionLayer::new()); Ok(router) diff --git a/src/service/http/middleware/mod.rs b/src/service/http/middleware/mod.rs index 13ead900..e37adb5e 100644 --- a/src/service/http/middleware/mod.rs +++ b/src/service/http/middleware/mod.rs @@ -8,6 +8,7 @@ pub mod timeout; pub mod tracing; use crate::app_context::AppContext; +use crate::error::RoadsterResult; use axum::Router; /// Allows initializing and installing middleware on the app's [Router]. The type `S` is the @@ -43,5 +44,5 @@ pub trait Middleware: Send { /// with priority `-10` will be _installed after_ a middleware with priority `10`, which will /// allow the middleware with priority `-10` to _run before_ a middleware with priority `10`. fn priority(&self, context: &AppContext) -> i32; - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result; + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult; } diff --git a/src/service/http/middleware/request_id.rs b/src/service/http/middleware/request_id.rs index 015ce44b..35a68dab 100644 --- a/src/service/http/middleware/request_id.rs +++ b/src/service/http/middleware/request_id.rs @@ -1,4 +1,5 @@ use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::middleware::Middleware; use axum::http::HeaderName; use axum::Router; @@ -66,7 +67,7 @@ impl Middleware for SetRequestIdMiddleware { .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let header_name = &context .config() .service @@ -117,7 +118,7 @@ impl Middleware for PropagateRequestIdMiddleware { .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let header_name = &context .config() .service diff --git a/src/service/http/middleware/sensitive_headers.rs b/src/service/http/middleware/sensitive_headers.rs index 830b5184..94b4ca24 100644 --- a/src/service/http/middleware/sensitive_headers.rs +++ b/src/service/http/middleware/sensitive_headers.rs @@ -6,6 +6,7 @@ use itertools::Itertools; use serde_derive::{Deserialize, Serialize}; use std::str::FromStr; +use crate::error::RoadsterResult; use tower_http::sensitive_headers::{ SetSensitiveRequestHeadersLayer, SetSensitiveResponseHeadersLayer, }; @@ -30,7 +31,7 @@ impl Default for CommonSensitiveHeadersConfig { } impl CommonSensitiveHeadersConfig { - pub fn header_names(&self) -> anyhow::Result> { + pub fn header_names(&self) -> RoadsterResult> { let header_names = self .header_names .iter() @@ -83,7 +84,7 @@ impl Middleware for SensitiveRequestHeadersMiddlewa .common .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let headers = context .config() .service @@ -130,7 +131,7 @@ impl Middleware for SensitiveResponseHeadersMiddlew .common .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let headers = context .config() .service diff --git a/src/service/http/middleware/size_limit.rs b/src/service/http/middleware/size_limit.rs index 7f09ec9c..b7c1cf56 100644 --- a/src/service/http/middleware/size_limit.rs +++ b/src/service/http/middleware/size_limit.rs @@ -1,6 +1,7 @@ use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::middleware::Middleware; -use anyhow::bail; +use anyhow::anyhow; use axum::Router; use byte_unit::rust_decimal::prelude::ToPrimitive; use byte_unit::Byte; @@ -52,7 +53,7 @@ impl Middleware for RequestBodyLimitMiddleware { .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let limit = &context .config() .service @@ -68,7 +69,7 @@ impl Middleware for RequestBodyLimitMiddleware { // Todo: is there a cleaner way to write this? let limit = match limit { Some(limit) => limit, - None => bail!("Unable to convert bytes from u64 to usize"), + None => return Err(anyhow!("Unable to convert bytes from u64 to usize").into()), }; let router = router.layer(RequestBodyLimitLayer::new(*limit)); diff --git a/src/service/http/middleware/timeout.rs b/src/service/http/middleware/timeout.rs index 704078cc..eee323f6 100644 --- a/src/service/http/middleware/timeout.rs +++ b/src/service/http/middleware/timeout.rs @@ -1,4 +1,5 @@ use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::middleware::Middleware; use axum::Router; use serde_derive::{Deserialize, Serialize}; @@ -52,7 +53,7 @@ impl Middleware for TimeoutMiddleware { .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let timeout = &context .config() .service diff --git a/src/service/http/middleware/tracing.rs b/src/service/http/middleware/tracing.rs index 942498a3..3741ccf9 100644 --- a/src/service/http/middleware/tracing.rs +++ b/src/service/http/middleware/tracing.rs @@ -1,4 +1,5 @@ use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::http::middleware::Middleware; use axum::extract::MatchedPath; use axum::http::{Request, Response}; @@ -45,7 +46,7 @@ impl Middleware for TracingMiddleware { .priority } - fn install(&self, router: Router, context: &AppContext) -> anyhow::Result { + fn install(&self, router: Router, context: &AppContext) -> RoadsterResult { let request_id_header_name = &context .config() .service diff --git a/src/service/http/service.rs b/src/service/http/service.rs index 4e5b44e9..d5bc49e9 100644 --- a/src/service/http/service.rs +++ b/src/service/http/service.rs @@ -6,6 +6,7 @@ use crate::cli::roadster::RoadsterCli; use crate::cli::roadster::RoadsterCommand; #[cfg(all(feature = "cli", feature = "open-api"))] use crate::cli::roadster::RoadsterSubCommand; +use crate::error::RoadsterResult; use crate::service::http::builder::HttpServiceBuilder; use crate::service::AppService; #[cfg(feature = "open-api")] @@ -47,7 +48,7 @@ impl AppService for HttpService { roadster_cli: &RoadsterCli, _app_cli: &A::Cli, _app_context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { if let Some(command) = roadster_cli.command.as_ref() { match command { RoadsterCommand::Roadster(args) => match &args.command { @@ -75,7 +76,7 @@ impl AppService for HttpService { &self, app_context: &AppContext, cancel_token: CancellationToken, - ) -> anyhow::Result<()> { + ) -> RoadsterResult<()> { let server_addr = app_context.config().service.http.custom.address.url(); info!("Server will start at {server_addr}"); @@ -114,7 +115,7 @@ impl HttpService { &self, pretty_print: bool, output: Option<&PathBuf>, - ) -> anyhow::Result<()> { + ) -> RoadsterResult<()> { let schema_json = if pretty_print { serde_json::to_string_pretty(self.api.as_ref())? } else { diff --git a/src/service/mod.rs b/src/service/mod.rs index 8e147b84..1dc880b7 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::app_context::AppContext; #[cfg(feature = "cli")] use crate::cli::roadster::RoadsterCli; +use crate::error::RoadsterResult; use async_trait::async_trait; use tokio_util::sync::CancellationToken; @@ -42,7 +43,7 @@ pub trait AppService: Send + Sync { _roadster_cli: &RoadsterCli, _app_cli: &A::Cli, _app_context: &AppContext, - ) -> anyhow::Result { + ) -> RoadsterResult { Ok(false) } @@ -54,7 +55,7 @@ pub trait AppService: Send + Sync { &self, app_context: &AppContext, cancel_token: CancellationToken, - ) -> anyhow::Result<()>; + ) -> RoadsterResult<()>; } /// Trait used to build an [AppService]. It's not a requirement that services implement this @@ -73,13 +74,14 @@ where S::enabled(app_context) } - async fn build(self, context: &AppContext) -> anyhow::Result; + async fn build(self, context: &AppContext) -> RoadsterResult; } #[cfg(test)] mod tests { use crate::app::MockApp; use crate::app_context::AppContext; + use crate::error::RoadsterResult; use crate::service::{AppServiceBuilder, MockAppService}; use async_trait::async_trait; use rstest::rstest; @@ -88,7 +90,7 @@ mod tests { #[async_trait] impl AppServiceBuilder> for TestAppServiceBuilder { #[cfg_attr(coverage_nightly, coverage(off))] - async fn build(self, _context: &AppContext<()>) -> anyhow::Result> { + async fn build(self, _context: &AppContext<()>) -> RoadsterResult> { Ok(MockAppService::default()) } } diff --git a/src/service/registry.rs b/src/service/registry.rs index 0a45a65c..4d838b9b 100644 --- a/src/service/registry.rs +++ b/src/service/registry.rs @@ -1,7 +1,8 @@ use crate::app::App; use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::{AppService, AppServiceBuilder}; -use anyhow::bail; +use anyhow::anyhow; use std::collections::BTreeMap; use tracing::info; @@ -24,7 +25,7 @@ impl ServiceRegistry { /// Register a new service. If the service is not enabled (e.g., [AppService::enabled] is `false`), /// the service will not be registered. - pub fn register_service(&mut self, service: S) -> anyhow::Result<()> + pub fn register_service(&mut self, service: S) -> RoadsterResult<()> where S: AppService + 'static, { @@ -37,7 +38,7 @@ impl ServiceRegistry { /// Build and register a new service. If the service is not enabled (e.g., /// [AppService::enabled] is `false`), the service will not be built or registered. - pub async fn register_builder(&mut self, builder: B) -> anyhow::Result<()> + pub async fn register_builder(&mut self, builder: B) -> RoadsterResult<()> where S: AppService + 'static, B: AppServiceBuilder, @@ -53,14 +54,14 @@ impl ServiceRegistry { self.register_internal(service) } - fn register_internal(&mut self, service: S) -> anyhow::Result<()> + fn register_internal(&mut self, service: S) -> RoadsterResult<()> where S: AppService + 'static, { info!(service = %S::name(), "Registering service"); if self.services.insert(S::name(), Box::new(service)).is_some() { - bail!("Service `{}` was already registered", S::name()); + return Err(anyhow!("Service `{}` was already registered", S::name()).into()); } Ok(()) } diff --git a/src/service/runner.rs b/src/service/runner.rs index 91ff029a..d5dced3b 100644 --- a/src/service/runner.rs +++ b/src/service/runner.rs @@ -2,6 +2,7 @@ use crate::app::App; use crate::app_context::AppContext; #[cfg(feature = "cli")] use crate::cli::roadster::RoadsterCli; +use crate::error::RoadsterResult; use crate::service::registry::ServiceRegistry; use std::future::Future; use tokio::task::JoinSet; @@ -14,7 +15,7 @@ pub(crate) async fn handle_cli( app_cli: &A::Cli, service_registry: &ServiceRegistry, context: &AppContext, -) -> anyhow::Result +) -> RoadsterResult where A: App, { @@ -29,7 +30,7 @@ where pub(crate) async fn run( service_registry: ServiceRegistry, context: &AppContext, -) -> anyhow::Result<()> +) -> RoadsterResult<()> where A: App, { @@ -143,7 +144,7 @@ where async fn cancel_token_on_signal_received( shutdown_signal: F, cancellation_token: CancellationToken, -) -> anyhow::Result<()> +) -> RoadsterResult<()> where F: Future + Send + 'static, { @@ -160,9 +161,9 @@ async fn cancel_on_error( cancellation_token: CancellationToken, context: &AppContext, f: F, -) -> anyhow::Result +) -> RoadsterResult where - F: Future> + Send + 'static, + F: Future> + Send + 'static, { let result = f.await; if result.is_err() && context.config().app.shutdown_on_error { @@ -177,10 +178,10 @@ async fn graceful_shutdown( app_graceful_shutdown: F2, // This parameter is (currently) not used when no features are enabled. #[allow(unused_variables)] context: AppContext, -) -> anyhow::Result<()> +) -> RoadsterResult<()> where F1: Future + Send + 'static, - F2: Future> + Send + 'static, + F2: Future> + Send + 'static, { shutdown_signal.await; diff --git a/src/service/worker/sidekiq/app_worker.rs b/src/service/worker/sidekiq/app_worker.rs index e082c8ba..f132591b 100644 --- a/src/service/worker/sidekiq/app_worker.rs +++ b/src/service/worker/sidekiq/app_worker.rs @@ -1,5 +1,6 @@ use crate::app::App; use crate::app_context::AppContext; +use crate::error::RoadsterResult; use async_trait::async_trait; use serde_derive::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none}; @@ -57,7 +58,7 @@ where /// Enqueue the worker into its Sidekiq queue. This is a helper method around [Worker::perform_async] /// so the caller can simply provide the [state][App::State] instead of needing to access the /// [sidekiq::RedisPool] from inside the [state][App::State]. - async fn enqueue(context: &AppContext, args: Args) -> anyhow::Result<()> { + async fn enqueue(context: &AppContext, args: Args) -> RoadsterResult<()> { Self::perform_async(context.redis_enqueue(), args).await?; Ok(()) } diff --git a/src/service/worker/sidekiq/builder.rs b/src/service/worker/sidekiq/builder.rs index cdcf27a8..8904f7e9 100644 --- a/src/service/worker/sidekiq/builder.rs +++ b/src/service/worker/sidekiq/builder.rs @@ -1,13 +1,14 @@ use crate::app::App; use crate::app_context::AppContext; use crate::config::service::worker::sidekiq::StaleCleanUpBehavior; +use crate::error::RoadsterResult; use crate::service::worker::sidekiq::app_worker::AppWorker; use crate::service::worker::sidekiq::roadster_worker::RoadsterWorker; use crate::service::worker::sidekiq::service::SidekiqWorkerService; #[cfg_attr(test, mockall_double::double)] use crate::service::worker::sidekiq::Processor; use crate::service::{AppService, AppServiceBuilder}; -use anyhow::{anyhow, bail}; +use anyhow::anyhow; use async_trait::async_trait; use bb8::PooledConnection; use itertools::Itertools; @@ -51,7 +52,7 @@ where } } - async fn build(self, context: &AppContext) -> anyhow::Result { + async fn build(self, context: &AppContext) -> RoadsterResult { let service = match self.state { BuilderState::Enabled { processor, @@ -66,7 +67,10 @@ where } } BuilderState::Disabled => { - bail!("This builder is not enabled; it's build method should not have been called.") + return Err(anyhow!( + "This builder is not enabled; it's build method should not have been called." + ) + .into()); } }; @@ -81,14 +85,14 @@ where pub async fn with_processor( context: &AppContext, processor: sidekiq::Processor, - ) -> anyhow::Result { + ) -> RoadsterResult { Self::new(context.clone(), Some(Processor::new(processor))).await } pub async fn with_default_processor( context: &AppContext, worker_queues: Option>, - ) -> anyhow::Result { + ) -> RoadsterResult { let processor = if !>::enabled(context) { debug!("Sidekiq service not enabled, not creating the Sidekiq processor"); None @@ -144,7 +148,7 @@ where async fn new( context: AppContext, processor: Option>, - ) -> anyhow::Result { + ) -> RoadsterResult { let processor = if >::enabled(&context) { processor } else { @@ -165,7 +169,7 @@ where Ok(Self { state }) } - async fn auto_clean_periodic(context: &AppContext) -> anyhow::Result<()> { + async fn auto_clean_periodic(context: &AppContext) -> RoadsterResult<()> { if context .config() .service @@ -191,7 +195,7 @@ where /// Periodic jobs can also be cleaned up automatically by setting the /// [service.sidekiq.periodic.stale-cleanup][crate::config::service::worker::sidekiq::StaleCleanUpBehavior] /// to `auto-clean-all` or `auto-clean-stale`. - pub async fn clean_up_periodic_jobs(self) -> anyhow::Result { + pub async fn clean_up_periodic_jobs(self) -> RoadsterResult { if let BuilderState::Enabled { registered_periodic_workers, context, @@ -199,7 +203,7 @@ where } = &self.state { if !registered_periodic_workers.is_empty() { - bail!("Can only clean up previous periodic jobs if no periodic jobs have been registered yet.") + return Err(anyhow!("Can only clean up previous periodic jobs if no periodic jobs have been registered yet.").into()); } periodic::destroy_all(context.redis_enqueue().clone()).await?; } @@ -211,7 +215,7 @@ where /// /// The worker will be wrapped by a [RoadsterWorker], which provides some common behavior, such /// as enforcing a timeout/max duration of worker jobs. - pub fn register_app_worker(mut self, worker: W) -> anyhow::Result + pub fn register_app_worker(mut self, worker: W) -> RoadsterResult where Args: Sync + Send + Serialize + for<'de> serde::Deserialize<'de> + 'static, W: AppWorker + 'static, @@ -226,7 +230,7 @@ where let class_name = W::class_name(); debug!(worker = %class_name, "Registering worker"); if !registered_workers.insert(class_name.clone()) { - bail!("Worker `{class_name}` was already registered"); + return Err(anyhow!("Worker `{class_name}` was already registered").into()); } let roadster_worker = RoadsterWorker::new(worker, context); processor.register(roadster_worker); @@ -247,7 +251,7 @@ where builder: periodic::Builder, worker: W, args: Args, - ) -> anyhow::Result + ) -> RoadsterResult where Args: Sync + Send + Serialize + for<'de> serde::Deserialize<'de> + 'static, W: AppWorker + 'static, @@ -265,9 +269,10 @@ where let builder = builder.args(args)?; let job_json = serde_json::to_string(&builder.into_periodic_job(class_name.clone())?)?; if !registered_periodic_workers.insert(job_json.clone()) { - bail!( + return Err(anyhow!( "Periodic worker `{class_name}` was already registered; full job: {job_json}" - ); + ) + .into()); } processor .register_periodic(builder, roadster_worker) @@ -290,7 +295,7 @@ async fn remove_stale_periodic_jobs( conn: &mut C, context: &AppContext, registered_periodic_workers: &HashSet, -) -> anyhow::Result<()> { +) -> RoadsterResult<()> { let stale_jobs = conn .zrange(PERIODIC_KEY.to_string(), 0, -1) .await? diff --git a/src/service/worker/sidekiq/mod.rs b/src/service/worker/sidekiq/mod.rs index f487e84a..b7ba24a8 100644 --- a/src/service/worker/sidekiq/mod.rs +++ b/src/service/worker/sidekiq/mod.rs @@ -1,4 +1,5 @@ use crate::app::App; +use crate::error::RoadsterResult; use crate::service::worker::sidekiq::app_worker::AppWorker; use crate::service::worker::sidekiq::roadster_worker::RoadsterWorker; use serde::Serialize; @@ -48,7 +49,7 @@ where &mut self, builder: periodic::Builder, worker: RoadsterWorker, - ) -> anyhow::Result<()> + ) -> RoadsterResult<()> where Args: Sync + Send + Serialize + for<'de> serde::Deserialize<'de> + 'static, W: AppWorker + 'static, @@ -77,7 +78,7 @@ mockall::mock! { &mut self, builder: periodic::Builder, worker: RoadsterWorker, - ) -> anyhow::Result<()> + ) -> RoadsterResult<()> where Args: Sync + Send + Serialize + for<'de> serde::Deserialize<'de> + 'static, W: AppWorker + 'static; diff --git a/src/service/worker/sidekiq/service.rs b/src/service/worker/sidekiq/service.rs index 1e7053f3..5421c9d5 100644 --- a/src/service/worker/sidekiq/service.rs +++ b/src/service/worker/sidekiq/service.rs @@ -1,5 +1,6 @@ use crate::app::App; use crate::app_context::AppContext; +use crate::error::RoadsterResult; use crate::service::worker::sidekiq::builder::SidekiqWorkerServiceBuilder; use crate::service::AppService; use async_trait::async_trait; @@ -49,7 +50,7 @@ impl AppService for SidekiqWorkerService { &self, _app_context: &AppContext, cancel_token: CancellationToken, - ) -> anyhow::Result<()> { + ) -> RoadsterResult<()> { let processor = self.processor.clone(); let sidekiq_cancel_token = processor.get_cancellation_token(); @@ -81,7 +82,7 @@ impl AppService for SidekiqWorkerService { impl SidekiqWorkerService { pub async fn builder( context: &AppContext, - ) -> anyhow::Result> + ) -> RoadsterResult> where A: App + 'static, { diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index 4ccbf294..fa193266 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -20,9 +20,10 @@ use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; use crate::config::app_config::AppConfig; +use crate::error::RoadsterResult; // Todo: make this configurable -pub fn init_tracing(config: &AppConfig) -> anyhow::Result<()> { +pub fn init_tracing(config: &AppConfig) -> RoadsterResult<()> { // Stdout Layer let stdout_layer = tracing_subscriber::fmt::layer(); diff --git a/src/view/http/app_error.rs b/src/view/http/app_error.rs deleted file mode 100644 index 624c2dc1..00000000 --- a/src/view/http/app_error.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Mostly copied from - -#[cfg(feature = "open-api")] -use aide::OperationIo; -use axum::{http::StatusCode, response::IntoResponse}; -#[cfg(feature = "open-api")] -use schemars::JsonSchema; -use serde::Serialize; -use serde_derive::Deserialize; -use serde_json::Value; -use std::fmt::Debug; -use tracing::error; - -/// A default error response for most API errors. -/// Todo: Helpers for various status codes. -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "open-api", derive(OperationIo, JsonSchema))] -pub struct AppError { - /// An error message. - pub error: String, - #[serde(skip)] - pub status: StatusCode, - /// Optional Additional error details. - #[serde(skip_serializing_if = "Option::is_none")] - pub error_details: Option, -} - -impl AppError { - pub fn new(error: &str) -> Self { - Self { - error: error.to_string(), - status: StatusCode::BAD_REQUEST, - error_details: None, - } - } - - pub fn with_status(mut self, status: StatusCode) -> Self { - self.status = status; - self - } - - pub fn with_details(mut self, details: Value) -> Self { - self.error_details = Some(details); - self - } -} - -impl IntoResponse for AppError { - fn into_response(self) -> axum::response::Response { - let status = self.status; - let mut res = axum::Json(self).into_response(); - *res.status_mut() = status; - res - } -} - -/// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into -/// `Result<_, AppError>`. That way you don't need to do that manually. -impl From for AppError -where - E: Into, -{ - fn from(err: E) -> Self { - // By default, we don't want to return the details from the anyhow::Error to the user, - // so we just emit the error as a trace and return a generic error message. - // Todo: Figure out a good way to return some details while ensuring we don't return - // any sensitive details. - error!("{}", err.into()); - Self { - error: "Something went wrong".to_string(), - status: StatusCode::INTERNAL_SERVER_ERROR, - error_details: None, - } - } -} diff --git a/src/view/http/mod.rs b/src/view/http/mod.rs deleted file mode 100644 index 441553cb..00000000 --- a/src/view/http/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod app_error; diff --git a/src/view/mod.rs b/src/view/mod.rs deleted file mode 100644 index d56a2d39..00000000 --- a/src/view/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(feature = "http")] -pub mod http;