public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate
@ 2023-07-26 14:18 Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 1/4] http-error: add new http-error crate Lukas Wagner
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Lukas Wagner @ 2023-07-26 14:18 UTC (permalink / raw)
  To: pve-devel

This patch series moves `HttpError` from `proxmox-router` into its own little 
crate. The main benefit of this is that one can now use `HttpError`
independently (e.g. for `proxmox-notify`)

Changes since v1:
  - Re-export HttpError from proxmox-router instead of changing all
    existing users.
  - Drop unused 'derive' features for the serde dependency



proxmox:

Lukas Wagner (3):
  http-error: add new http-error crate
  router: re-export `HttpError` from `proxmox-http-error`
  notify: use HttpError from proxmox-http-error

 Cargo.toml                         |   2 +
 proxmox-http-error/Cargo.toml      |  16 ++++
 proxmox-http-error/src/lib.rs      |  81 ++++++++++++++++++
 proxmox-notify/Cargo.toml          |   1 +
 proxmox-notify/src/api/common.rs   |  33 ++++----
 proxmox-notify/src/api/filter.rs   |  59 ++++++-------
 proxmox-notify/src/api/gotify.rs   |  85 ++++++++++---------
 proxmox-notify/src/api/group.rs    |  82 +++++++++---------
 proxmox-notify/src/api/mod.rs      | 130 ++++++++++-------------------
 proxmox-notify/src/api/sendmail.rs |  93 ++++++++++++---------
 proxmox-router/Cargo.toml          |   1 +
 proxmox-router/src/error.rs        |  42 +---------
 proxmox-router/src/lib.rs          |   2 +-
 13 files changed, 334 insertions(+), 293 deletions(-)
 create mode 100644 proxmox-http-error/Cargo.toml
 create mode 100644 proxmox-http-error/src/lib.rs


proxmox-perl-rs:

Lukas Wagner (1):
  notify: use new HttpError type

 common/src/notify.rs | 77 +++++++++++++++++++++++---------------------
 pve-rs/Cargo.toml    |  1 +
 2 files changed, 42 insertions(+), 36 deletions(-)


Summary over all repositories:
  15 files changed, 376 insertions(+), 329 deletions(-)

-- 
murpp v0.4.0





^ permalink raw reply	[flat|nested] 6+ messages in thread

* [pve-devel] [PATCH v2 proxmox 1/4] http-error: add new http-error crate
  2023-07-26 14:18 [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Lukas Wagner
@ 2023-07-26 14:18 ` Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 2/4] router: re-export `HttpError` from `proxmox-http-error` Lukas Wagner
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Lukas Wagner @ 2023-07-26 14:18 UTC (permalink / raw)
  To: pve-devel; +Cc: Lukas Wagner, Wolfgang Bumiller

Break out proxmox-router's HttpError into it's own crate so that it can
be used without pulling in proxmox-router.

This commit also implements `Serialize` for `HttpError` so that it can
be returned from perlmod bindings, allowing Perl code to access the
status code as well as the message.

Also add some smoke-tests to make sure that the `http_bail` and
`http_err` macros actually produce valid code.

Suggested-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 Cargo.toml                    |  2 +
 proxmox-http-error/Cargo.toml | 16 +++++++
 proxmox-http-error/src/lib.rs | 81 +++++++++++++++++++++++++++++++++++
 3 files changed, 99 insertions(+)
 create mode 100644 proxmox-http-error/Cargo.toml
 create mode 100644 proxmox-http-error/src/lib.rs

diff --git a/Cargo.toml b/Cargo.toml
index 54be5f6..cb82253 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
     "proxmox-borrow",
     "proxmox-compression",
     "proxmox-http",
+    "proxmox-http-error",
     "proxmox-human-byte",
     "proxmox-io",
     "proxmox-lang",
@@ -88,6 +89,7 @@ proxmox-api-macro = { version = "1.0.4", path = "proxmox-api-macro" }
 proxmox-async = { version = "0.4.1", path = "proxmox-async" }
 proxmox-compression = { version = "0.2.0", path = "proxmox-compression" }
 proxmox-http = { version = "0.9.0", path = "proxmox-http" }
+proxmox-http-error = { version = "0.1.0", path = "proxmox-http-error" }
 proxmox-human-byte = { version = "0.1.0", path = "proxmox-human-byte" }
 proxmox-io = { version = "1.0.0", path = "proxmox-io" }
 proxmox-lang = { version = "1.1", path = "proxmox-lang" }
diff --git a/proxmox-http-error/Cargo.toml b/proxmox-http-error/Cargo.toml
new file mode 100644
index 0000000..6f54bfc
--- /dev/null
+++ b/proxmox-http-error/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "proxmox-http-error"
+version = "0.1.0"
+
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Proxmox HTTP Error"
+
+exclude.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+http.workspace = true
+serde.workspace = true
diff --git a/proxmox-http-error/src/lib.rs b/proxmox-http-error/src/lib.rs
new file mode 100644
index 0000000..de45cb9
--- /dev/null
+++ b/proxmox-http-error/src/lib.rs
@@ -0,0 +1,81 @@
+use serde::{ser::SerializeStruct, Serialize, Serializer};
+use std::fmt;
+
+#[doc(hidden)]
+pub use http::StatusCode;
+
+/// HTTP error including `StatusCode` and message.
+#[derive(Debug)]
+pub struct HttpError {
+    pub code: StatusCode,
+    pub message: String,
+}
+
+impl std::error::Error for HttpError {}
+
+impl HttpError {
+    pub fn new(code: StatusCode, message: String) -> Self {
+        HttpError { code, message }
+    }
+}
+
+impl fmt::Display for HttpError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.message)
+    }
+}
+
+impl Serialize for HttpError {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut state = serializer.serialize_struct("HttpError", 2)?;
+        state.serialize_field("code", &self.code.as_u16())?;
+        state.serialize_field("message", &self.message)?;
+        state.end()
+    }
+}
+
+/// Macro to create a HttpError inside a anyhow::Error
+#[macro_export]
+macro_rules! http_err {
+    ($status:ident, $($fmt:tt)+) => {{
+        ::anyhow::Error::from($crate::HttpError::new(
+            $crate::StatusCode::$status,
+            format!($($fmt)+)
+        ))
+    }};
+}
+
+/// Bail with an error generated with the `http_err!` macro.
+#[macro_export]
+macro_rules! http_bail {
+    ($status:ident, $($fmt:tt)+) => {{
+        return Err($crate::http_err!($status, $($fmt)+));
+    }};
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_http_err() {
+        // Make sure the macro generates valid code.
+        http_err!(IM_A_TEAPOT, "Cannot brew coffee");
+    }
+
+    #[test]
+    fn test_http_bail() {
+        fn t() -> Result<(), anyhow::Error> {
+            // Make sure the macro generates valid code.
+            http_bail!(
+                UNAVAILABLE_FOR_LEGAL_REASONS,
+                "Nothing to see here, move along"
+            );
+        }
+
+        assert!(t().is_err());
+    }
+}
-- 
2.39.2





^ permalink raw reply	[flat|nested] 6+ messages in thread

* [pve-devel] [PATCH v2 proxmox 2/4] router: re-export `HttpError` from `proxmox-http-error`
  2023-07-26 14:18 [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 1/4] http-error: add new http-error crate Lukas Wagner
@ 2023-07-26 14:18 ` Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 3/4] notify: use HttpError from proxmox-http-error Lukas Wagner
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Lukas Wagner @ 2023-07-26 14:18 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 proxmox-router/Cargo.toml   |  1 +
 proxmox-router/src/error.rs | 42 +------------------------------------
 proxmox-router/src/lib.rs   |  2 +-
 3 files changed, 3 insertions(+), 42 deletions(-)

diff --git a/proxmox-router/Cargo.toml b/proxmox-router/Cargo.toml
index 34a1985..7d4d32f 100644
--- a/proxmox-router/Cargo.toml
+++ b/proxmox-router/Cargo.toml
@@ -26,6 +26,7 @@ rustyline = { version = "9", optional = true }
 libc = { workspace = true, optional = true }
 
 proxmox-lang.workspace = true
+proxmox-http-error.workspace = true
 proxmox-schema.workspace = true
 proxmox-async.workspace = true
 
diff --git a/proxmox-router/src/error.rs b/proxmox-router/src/error.rs
index e285cf7..15c5417 100644
--- a/proxmox-router/src/error.rs
+++ b/proxmox-router/src/error.rs
@@ -1,44 +1,4 @@
-use std::fmt;
+pub use proxmox_http_error::{http_bail, http_err, HttpError};
 
 #[doc(hidden)]
 pub use http::StatusCode;
-
-/// HTTP error including `StatusCode` and message.
-#[derive(Debug)]
-pub struct HttpError {
-    pub code: StatusCode,
-    pub message: String,
-}
-
-impl std::error::Error for HttpError {}
-
-impl HttpError {
-    pub fn new(code: StatusCode, message: String) -> Self {
-        HttpError { code, message }
-    }
-}
-
-impl fmt::Display for HttpError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{}", self.message)
-    }
-}
-
-/// Macro to create a HttpError inside a anyhow::Error
-#[macro_export]
-macro_rules! http_err {
-    ($status:ident, $($fmt:tt)+) => {{
-        ::anyhow::Error::from($crate::HttpError::new(
-            $crate::error::StatusCode::$status,
-            format!($($fmt)+)
-        ))
-    }};
-}
-
-/// Bail with an error generated with the `http_err!` macro.
-#[macro_export]
-macro_rules! http_bail {
-    ($status:ident, $($fmt:tt)+) => {{
-        return Err($crate::http_err!($status, $($fmt)+));
-    }};
-}
diff --git a/proxmox-router/src/lib.rs b/proxmox-router/src/lib.rs
index 7f4f2d9..808836a 100644
--- a/proxmox-router/src/lib.rs
+++ b/proxmox-router/src/lib.rs
@@ -17,7 +17,7 @@ mod serializable_return;
 
 #[doc(inline)]
 #[cfg(feature = "server")]
-pub use error::HttpError;
+pub use error::*;
 
 pub use permission::*;
 pub use router::*;
-- 
2.39.2





^ permalink raw reply	[flat|nested] 6+ messages in thread

* [pve-devel] [PATCH v2 proxmox 3/4] notify: use HttpError from proxmox-http-error
  2023-07-26 14:18 [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 1/4] http-error: add new http-error crate Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 2/4] router: re-export `HttpError` from `proxmox-http-error` Lukas Wagner
@ 2023-07-26 14:18 ` Lukas Wagner
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox-perl-rs 4/4] notify: use new HttpError type Lukas Wagner
  2023-07-28  9:58 ` [pve-devel] applied-series: [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Wolfgang Bumiller
  4 siblings, 0 replies; 6+ messages in thread
From: Lukas Wagner @ 2023-07-26 14:18 UTC (permalink / raw)
  To: pve-devel

Also improve API documentation in terms of which HttpError is
returned when.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---

Notes:
    Assumes that the following two patches have been applied before,
    otherwise there will be a conflict (which is trivial to resolve,
    though)
    
    1: "notify: fix build warnings if not all features are enabled":
        https://lists.proxmox.com/pipermail/pve-devel/2023-July/058409.html
    
    2: "notify: fix tests if not all features are enabled"
        https://lists.proxmox.com/pipermail/pve-devel/2023-July/058410.html

 proxmox-notify/Cargo.toml          |   1 +
 proxmox-notify/src/api/common.rs   |  33 ++++----
 proxmox-notify/src/api/filter.rs   |  59 ++++++-------
 proxmox-notify/src/api/gotify.rs   |  85 ++++++++++---------
 proxmox-notify/src/api/group.rs    |  82 +++++++++---------
 proxmox-notify/src/api/mod.rs      | 130 ++++++++++-------------------
 proxmox-notify/src/api/sendmail.rs |  93 ++++++++++++---------
 7 files changed, 232 insertions(+), 251 deletions(-)

diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index 5cceb0b..8b36a52 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -14,6 +14,7 @@ log.workspace = true
 once_cell.workspace = true
 openssl.workspace = true
 proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
+proxmox-http-error.workspace = true
 proxmox-human-byte.workspace = true
 proxmox-schema = { workspace = true, features = ["api-macro", "api-types"]}
 proxmox-section-config = { workspace = true }
diff --git a/proxmox-notify/src/api/common.rs b/proxmox-notify/src/api/common.rs
index 48761fb..bc7029f 100644
--- a/proxmox-notify/src/api/common.rs
+++ b/proxmox-notify/src/api/common.rs
@@ -1,15 +1,17 @@
-use crate::api::ApiError;
+use super::http_err;
 use crate::{Bus, Config, Notification};
 
+use proxmox_http_error::HttpError;
+
 /// Send a notification to a given target.
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns an `ApiError` in case of an error.
-pub fn send(config: &Config, channel: &str, notification: &Notification) -> Result<(), ApiError> {
+/// Returns an `anyhow::Error` in case of an error.
+pub fn send(config: &Config, channel: &str, notification: &Notification) -> Result<(), HttpError> {
     let bus = Bus::from_config(config).map_err(|err| {
-        ApiError::internal_server_error(
-            "Could not instantiate notification bus",
-            Some(Box::new(err)),
+        http_err!(
+            INTERNAL_SERVER_ERROR,
+            "Could not instantiate notification bus: {err}"
         )
     })?;
 
@@ -21,23 +23,20 @@ pub fn send(config: &Config, channel: &str, notification: &Notification) -> Resu
 /// Test target (group or single endpoint) identified by its `name`.
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns an `ApiError` if sending via the endpoint failed.
-pub fn test_target(config: &Config, endpoint: &str) -> Result<(), ApiError> {
+/// Returns an `anyhow::Error` if sending via the endpoint failed.
+pub fn test_target(config: &Config, endpoint: &str) -> Result<(), HttpError> {
     let bus = Bus::from_config(config).map_err(|err| {
-        ApiError::internal_server_error(
-            "Could not instantiate notification bus",
-            Some(Box::new(err)),
+        http_err!(
+            INTERNAL_SERVER_ERROR,
+            "Could not instantiate notification bus: {err}"
         )
     })?;
 
     bus.test_target(endpoint).map_err(|err| match err {
         crate::Error::TargetDoesNotExist(endpoint) => {
-            ApiError::not_found(format!("endpoint '{endpoint}' does not exist"), None)
+            http_err!(NOT_FOUND, "endpoint '{endpoint}' does not exist")
         }
-        _ => ApiError::internal_server_error(
-            format!("Could not test target: {err}"),
-            Some(Box::new(err)),
-        ),
+        _ => http_err!(INTERNAL_SERVER_ERROR, "Could not test target: {err}"),
     })?;
 
     Ok(())
@@ -49,7 +48,7 @@ pub fn test_target(config: &Config, endpoint: &str) -> Result<(), ApiError> {
 /// the result for 'grp1' would be [grp1, a, b, c, filter1, filter2].
 /// The result will always contain the entity that was passed as a parameter.
 /// If the entity does not exist, the result will only contain the entity.
-pub fn get_referenced_entities(config: &Config, entity: &str) -> Result<Vec<String>, ApiError> {
+pub fn get_referenced_entities(config: &Config, entity: &str) -> Result<Vec<String>, HttpError> {
     let entities = super::get_referenced_entities(config, entity);
     Ok(Vec::from_iter(entities.into_iter()))
 }
diff --git a/proxmox-notify/src/api/filter.rs b/proxmox-notify/src/api/filter.rs
index b5b6284..be0c25b 100644
--- a/proxmox-notify/src/api/filter.rs
+++ b/proxmox-notify/src/api/filter.rs
@@ -1,63 +1,69 @@
-use crate::api::ApiError;
+use crate::api::http_err;
 use crate::filter::{DeleteableFilterProperty, FilterConfig, FilterConfigUpdater, FILTER_TYPENAME};
 use crate::Config;
+use proxmox_http_error::HttpError;
 
 /// Get a list of all filters
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns a list of all filters or an `ApiError` if the config is erroneous.
-pub fn get_filters(config: &Config) -> Result<Vec<FilterConfig>, ApiError> {
+/// Returns a list of all filters or a `HttpError` if the config is
+/// (`500 Internal server error`).
+pub fn get_filters(config: &Config) -> Result<Vec<FilterConfig>, HttpError> {
     config
         .config
         .convert_to_typed_array(FILTER_TYPENAME)
-        .map_err(|e| ApiError::internal_server_error("Could not fetch filters", Some(e.into())))
+        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "Could not fetch filters: {e}"))
 }
 
 /// Get filter with given `name`
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns the endpoint or an `ApiError` if the filter was not found.
-pub fn get_filter(config: &Config, name: &str) -> Result<FilterConfig, ApiError> {
+/// Returns the endpoint or a `HttpError` if the filter was not found (`404 Not found`).
+pub fn get_filter(config: &Config, name: &str) -> Result<FilterConfig, HttpError> {
     config
         .config
         .lookup(FILTER_TYPENAME, name)
-        .map_err(|_| ApiError::not_found(format!("filter '{name}' not found"), None))
+        .map_err(|_| http_err!(NOT_FOUND, "filter '{name}' not found"))
 }
 
 /// Add new notification filter.
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if a filter with the same name already exists or
-/// if the filter could not be saved.
-pub fn add_filter(config: &mut Config, filter_config: &FilterConfig) -> Result<(), ApiError> {
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+pub fn add_filter(config: &mut Config, filter_config: &FilterConfig) -> Result<(), HttpError> {
     super::ensure_unique(config, &filter_config.name)?;
 
     config
         .config
         .set_data(&filter_config.name, FILTER_TYPENAME, filter_config)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save filter '{}'", filter_config.name),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save filter '{}': {e}",
+                filter_config.name
             )
         })?;
 
     Ok(())
 }
 
-/// Update existing filter
+/// Update existing notification filter
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the config could not be saved.
+/// Returns a `HttpError` if:
+///   - the configuration could not be saved (`500 Internal server error`)
+///   - an invalid digest was passed (`400 Bad request`)
 pub fn update_filter(
     config: &mut Config,
     name: &str,
     filter_updater: &FilterConfigUpdater,
     delete: Option<&[DeleteableFilterProperty]>,
     digest: Option<&[u8]>,
-) -> Result<(), ApiError> {
+) -> Result<(), HttpError> {
     super::verify_digest(config, digest)?;
 
     let mut filter = get_filter(config, name)?;
@@ -92,12 +98,7 @@ pub fn update_filter(
     config
         .config
         .set_data(name, FILTER_TYPENAME, &filter)
-        .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save filter '{name}'"),
-                Some(e.into()),
-            )
-        })?;
+        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "could not save filter '{name}': {e}"))?;
 
     Ok(())
 }
@@ -106,8 +107,10 @@ pub fn update_filter(
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the filter does not exist.
-pub fn delete_filter(config: &mut Config, name: &str) -> Result<(), ApiError> {
+/// Returns a `HttpError` if:
+///   - the entity does not exist (`404 Not found`)
+///   - the filter is still referenced by another entity (`400 Bad request`)
+pub fn delete_filter(config: &mut Config, name: &str) -> Result<(), HttpError> {
     // Check if the filter exists
     let _ = get_filter(config, name)?;
     super::ensure_unused(config, name)?;
@@ -142,14 +145,14 @@ filter: filter2
     }
 
     #[test]
-    fn test_update_not_existing_returns_error() -> Result<(), ApiError> {
+    fn test_update_not_existing_returns_error() -> Result<(), HttpError> {
         let mut config = empty_config();
         assert!(update_filter(&mut config, "test", &Default::default(), None, None).is_err());
         Ok(())
     }
 
     #[test]
-    fn test_update_invalid_digest_returns_error() -> Result<(), ApiError> {
+    fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> {
         let mut config = config_with_two_filters();
         assert!(update_filter(
             &mut config,
@@ -164,7 +167,7 @@ filter: filter2
     }
 
     #[test]
-    fn test_filter_update() -> Result<(), ApiError> {
+    fn test_filter_update() -> Result<(), HttpError> {
         let mut config = config_with_two_filters();
 
         let digest = config.digest;
@@ -215,7 +218,7 @@ filter: filter2
     }
 
     #[test]
-    fn test_filter_delete() -> Result<(), ApiError> {
+    fn test_filter_delete() -> Result<(), HttpError> {
         let mut config = config_with_two_filters();
 
         delete_filter(&mut config, "filter1")?;
diff --git a/proxmox-notify/src/api/gotify.rs b/proxmox-notify/src/api/gotify.rs
index 521dd16..0ec48fd 100644
--- a/proxmox-notify/src/api/gotify.rs
+++ b/proxmox-notify/src/api/gotify.rs
@@ -1,4 +1,6 @@
-use crate::api::ApiError;
+use proxmox_http_error::HttpError;
+
+use crate::api::http_err;
 use crate::endpoints::gotify::{
     DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
     GotifyPrivateConfigUpdater, GOTIFY_TYPENAME,
@@ -8,36 +10,41 @@ use crate::Config;
 /// Get a list of all gotify endpoints.
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns a list of all gotify endpoints or an `ApiError` if the config is erroneous.
-pub fn get_endpoints(config: &Config) -> Result<Vec<GotifyConfig>, ApiError> {
+/// Returns a list of all gotify endpoints or a `HttpError` if the config is
+/// erroneous (`500 Internal server error`).
+pub fn get_endpoints(config: &Config) -> Result<Vec<GotifyConfig>, HttpError> {
     config
         .config
         .convert_to_typed_array(GOTIFY_TYPENAME)
-        .map_err(|e| ApiError::internal_server_error("Could not fetch endpoints", Some(e.into())))
+        .map_err(|e| http_err!(NOT_FOUND, "Could not fetch endpoints: {e}"))
 }
 
 /// Get gotify endpoint with given `name`
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns the endpoint or an `ApiError` if the endpoint was not found.
-pub fn get_endpoint(config: &Config, name: &str) -> Result<GotifyConfig, ApiError> {
+/// Returns the endpoint or a `HttpError` if the endpoint was not found (`404 Not found`).
+pub fn get_endpoint(config: &Config, name: &str) -> Result<GotifyConfig, HttpError> {
     config
         .config
         .lookup(GOTIFY_TYPENAME, name)
-        .map_err(|_| ApiError::not_found(format!("endpoint '{name}' not found"), None))
+        .map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))
 }
 
 /// Add a new gotify endpoint.
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if an endpoint with the same name already exists,
-/// or if the endpoint could not be saved.
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+///
+/// Panics if the names of the private config and the public config do not match.
 pub fn add_endpoint(
     config: &mut Config,
     endpoint_config: &GotifyConfig,
     private_endpoint_config: &GotifyPrivateConfig,
-) -> Result<(), ApiError> {
+) -> Result<(), HttpError> {
     if endpoint_config.name != private_endpoint_config.name {
         // Programming error by the user of the crate, thus we panic
         panic!("name for endpoint config and private config must be identical");
@@ -56,20 +63,22 @@ pub fn add_endpoint(
         .config
         .set_data(&endpoint_config.name, GOTIFY_TYPENAME, endpoint_config)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save endpoint '{}'", endpoint_config.name),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save endpoint '{}': {e}",
+                endpoint_config.name
             )
-        })?;
-
-    Ok(())
+        })
 }
 
 /// Update existing gotify endpoint
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the config could not be saved.
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
 pub fn update_endpoint(
     config: &mut Config,
     name: &str,
@@ -77,7 +86,7 @@ pub fn update_endpoint(
     private_endpoint_config_updater: &GotifyPrivateConfigUpdater,
     delete: Option<&[DeleteableGotifyProperty]>,
     digest: Option<&[u8]>,
-) -> Result<(), ApiError> {
+) -> Result<(), HttpError> {
     super::verify_digest(config, digest)?;
 
     let mut endpoint = get_endpoint(config, name)?;
@@ -120,21 +129,21 @@ pub fn update_endpoint(
         .config
         .set_data(name, GOTIFY_TYPENAME, &endpoint)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save endpoint '{name}'"),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save endpoint '{name}': {e}"
             )
-        })?;
-
-    Ok(())
+        })
 }
 
 /// Delete existing gotify endpoint
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the endpoint does not exist.
-pub fn delete_gotify_endpoint(config: &mut Config, name: &str) -> Result<(), ApiError> {
+/// Returns a `HttpError` if:
+///   - the entity does not exist (`404 Not found`)
+///   - the endpoint is still referenced by another entity (`400 Bad request`)
+pub fn delete_gotify_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError> {
     // Check if the endpoint exists
     let _ = get_endpoint(config, name)?;
     super::ensure_unused(config, name)?;
@@ -148,22 +157,20 @@ pub fn delete_gotify_endpoint(config: &mut Config, name: &str) -> Result<(), Api
 fn set_private_config_entry(
     config: &mut Config,
     private_config: &GotifyPrivateConfig,
-) -> Result<(), ApiError> {
+) -> Result<(), HttpError> {
     config
         .private_config
         .set_data(&private_config.name, GOTIFY_TYPENAME, private_config)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!(
-                    "could not save private config for endpoint '{}'",
-                    private_config.name
-                ),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save private config for endpoint '{}': {e}",
+                private_config.name
             )
         })
 }
 
-fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), ApiError> {
+fn remove_private_config_entry(config: &mut Config, name: &str) -> Result<(), HttpError> {
     config.private_config.sections.remove(name);
     Ok(())
 }
@@ -173,7 +180,7 @@ mod tests {
     use super::*;
     use crate::api::test_helpers::empty_config;
 
-    pub fn add_default_gotify_endpoint(config: &mut Config) -> Result<(), ApiError> {
+    pub fn add_default_gotify_endpoint(config: &mut Config) -> Result<(), HttpError> {
         add_endpoint(
             config,
             &GotifyConfig {
@@ -193,7 +200,7 @@ mod tests {
     }
 
     #[test]
-    fn test_update_not_existing_returns_error() -> Result<(), ApiError> {
+    fn test_update_not_existing_returns_error() -> Result<(), HttpError> {
         let mut config = empty_config();
 
         assert!(update_endpoint(
@@ -210,7 +217,7 @@ mod tests {
     }
 
     #[test]
-    fn test_update_invalid_digest_returns_error() -> Result<(), ApiError> {
+    fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_gotify_endpoint(&mut config)?;
 
@@ -228,7 +235,7 @@ mod tests {
     }
 
     #[test]
-    fn test_gotify_update() -> Result<(), ApiError> {
+    fn test_gotify_update() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_gotify_endpoint(&mut config)?;
 
@@ -279,7 +286,7 @@ mod tests {
     }
 
     #[test]
-    fn test_gotify_endpoint_delete() -> Result<(), ApiError> {
+    fn test_gotify_endpoint_delete() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_gotify_endpoint(&mut config)?;
 
diff --git a/proxmox-notify/src/api/group.rs b/proxmox-notify/src/api/group.rs
index d6b8f38..6fc71ea 100644
--- a/proxmox-notify/src/api/group.rs
+++ b/proxmox-notify/src/api/group.rs
@@ -1,43 +1,47 @@
-use crate::api::ApiError;
+use proxmox_http_error::HttpError;
+
+use crate::api::{http_bail, http_err};
 use crate::group::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater, GROUP_TYPENAME};
 use crate::Config;
 
 /// Get all notification groups
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns a list of all groups or an `ApiError` if the config is erroneous.
-pub fn get_groups(config: &Config) -> Result<Vec<GroupConfig>, ApiError> {
+/// Returns a list of all groups or a `HttpError` if the config is
+/// erroneous (`500 Internal server error`).
+pub fn get_groups(config: &Config) -> Result<Vec<GroupConfig>, HttpError> {
     config
         .config
         .convert_to_typed_array(GROUP_TYPENAME)
-        .map_err(|e| ApiError::internal_server_error("Could not fetch groups", Some(e.into())))
+        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "Could not fetch groups: {e}"))
 }
 
 /// Get group with given `name`
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns the endpoint or an `ApiError` if the group was not found.
-pub fn get_group(config: &Config, name: &str) -> Result<GroupConfig, ApiError> {
+/// Returns the endpoint or an `HttpError` if the group was not found (`404 Not found`).
+pub fn get_group(config: &Config, name: &str) -> Result<GroupConfig, HttpError> {
     config
         .config
         .lookup(GROUP_TYPENAME, name)
-        .map_err(|_| ApiError::not_found(format!("group '{name}' not found"), None))
+        .map_err(|_| http_err!(NOT_FOUND, "group '{name}' not found"))
 }
 
 /// Add a new group.
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if a group with the same name already exists, or
-/// if the group could not be saved
-pub fn add_group(config: &mut Config, group_config: &GroupConfig) -> Result<(), ApiError> {
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - no endpoints were passed (`400 Bad request`)
+///   - referenced endpoints do not exist (`404 Not found`)
+///   - the configuration could not be saved (`500 Internal server error`)
+pub fn add_group(config: &mut Config, group_config: &GroupConfig) -> Result<(), HttpError> {
     super::ensure_unique(config, &group_config.name)?;
 
     if group_config.endpoint.is_empty() {
-        return Err(ApiError::bad_request(
-            "group must contain at least one endpoint",
-            None,
-        ));
+        http_bail!(BAD_REQUEST, "group must contain at least one endpoint",);
     }
 
     if let Some(filter) = &group_config.filter {
@@ -51,27 +55,31 @@ pub fn add_group(config: &mut Config, group_config: &GroupConfig) -> Result<(),
         .config
         .set_data(&group_config.name, GROUP_TYPENAME, group_config)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save group '{}'", group_config.name),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save group '{}': {e}",
+                group_config.name
             )
-        })?;
-
-    Ok(())
+        })
 }
 
 /// Update existing group
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the config could not be saved.
+/// Returns a `HttpError` if:
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - an invalid digest was passed (`400 Bad request`)
+///   - no endpoints were passed (`400 Bad request`)
+///   - referenced endpoints do not exist (`404 Not found`)
+///   - the configuration could not be saved (`500 Internal server error`)
 pub fn update_group(
     config: &mut Config,
     name: &str,
     updater: &GroupConfigUpdater,
     delete: Option<&[DeleteableGroupProperty]>,
     digest: Option<&[u8]>,
-) -> Result<(), ApiError> {
+) -> Result<(), HttpError> {
     super::verify_digest(config, digest)?;
 
     let mut group = get_group(config, name)?;
@@ -88,10 +96,7 @@ pub fn update_group(
     if let Some(endpoints) = &updater.endpoint {
         super::ensure_endpoints_exist(config, endpoints)?;
         if endpoints.is_empty() {
-            return Err(ApiError::bad_request(
-                "group must contain at least one endpoint",
-                None,
-            ));
+            http_bail!(BAD_REQUEST, "group must contain at least one endpoint",);
         }
         group.endpoint = endpoints.iter().map(Into::into).collect()
     }
@@ -109,22 +114,15 @@ pub fn update_group(
     config
         .config
         .set_data(name, GROUP_TYPENAME, &group)
-        .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save group '{name}'"),
-                Some(e.into()),
-            )
-        })?;
-
-    Ok(())
+        .map_err(|e| http_err!(INTERNAL_SERVER_ERROR, "could not save group '{name}': {e}"))
 }
 
 /// Delete existing group
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the group does not exist.
-pub fn delete_group(config: &mut Config, name: &str) -> Result<(), ApiError> {
+/// Returns a `HttpError` if the group does not exist (`404 Not found`).
+pub fn delete_group(config: &mut Config, name: &str) -> Result<(), HttpError> {
     // Check if the group exists
     let _ = get_group(config, name)?;
 
@@ -141,7 +139,7 @@ mod tests {
     use crate::api::sendmail::tests::add_sendmail_endpoint_for_test;
     use crate::api::test_helpers::*;
 
-    fn add_default_group(config: &mut Config) -> Result<(), ApiError> {
+    fn add_default_group(config: &mut Config) -> Result<(), HttpError> {
         add_sendmail_endpoint_for_test(config, "test")?;
 
         add_group(
@@ -173,14 +171,14 @@ mod tests {
     }
 
     #[test]
-    fn test_add_group() -> Result<(), ApiError> {
+    fn test_add_group() -> Result<(), HttpError> {
         let mut config = empty_config();
         assert!(add_default_group(&mut config).is_ok());
         Ok(())
     }
 
     #[test]
-    fn test_update_group_fails_if_endpoint_does_not_exist() -> Result<(), ApiError> {
+    fn test_update_group_fails_if_endpoint_does_not_exist() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_group(&mut config)?;
 
@@ -199,7 +197,7 @@ mod tests {
     }
 
     #[test]
-    fn test_update_group_fails_if_digest_invalid() -> Result<(), ApiError> {
+    fn test_update_group_fails_if_digest_invalid() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_group(&mut config)?;
 
@@ -215,7 +213,7 @@ mod tests {
     }
 
     #[test]
-    fn test_update_group() -> Result<(), ApiError> {
+    fn test_update_group() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_group(&mut config)?;
 
@@ -249,7 +247,7 @@ mod tests {
     }
 
     #[test]
-    fn test_group_delete() -> Result<(), ApiError> {
+    fn test_group_delete() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_default_group(&mut config)?;
 
diff --git a/proxmox-notify/src/api/mod.rs b/proxmox-notify/src/api/mod.rs
index 9372443..cb0e085 100644
--- a/proxmox-notify/src/api/mod.rs
+++ b/proxmox-notify/src/api/mod.rs
@@ -1,9 +1,6 @@
-use std::collections::HashSet;
-use std::error::Error as StdError;
-use std::fmt::Display;
-
 use crate::Config;
-use serde::Serialize;
+use proxmox_http_error::HttpError;
+use std::collections::HashSet;
 
 pub mod common;
 pub mod filter;
@@ -13,81 +10,44 @@ pub mod group;
 #[cfg(feature = "sendmail")]
 pub mod sendmail;
 
-#[derive(Debug, Serialize)]
-pub struct ApiError {
-    /// HTTP Error code
-    code: u16,
-    /// Error message
-    message: String,
-    #[serde(skip_serializing)]
-    /// The underlying cause of the error
-    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
+// We have our own, local versions of http_err and http_bail, because
+// we don't want to wrap the error in anyhow::Error. If we were to do that,
+// we would need to downcast in the perlmod bindings, since we need
+// to return `HttpError` from there.
+#[macro_export]
+macro_rules! http_err {
+    ($status:ident, $($fmt:tt)+) => {{
+        proxmox_http_error::HttpError::new(
+            proxmox_http_error::StatusCode::$status,
+            format!($($fmt)+)
+        )
+    }};
 }
 
-impl ApiError {
-    fn new<S: AsRef<str>>(
-        message: S,
-        code: u16,
-        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
-    ) -> Self {
-        Self {
-            message: message.as_ref().into(),
-            code,
-            source,
-        }
-    }
-
-    pub fn bad_request<S: AsRef<str>>(
-        message: S,
-        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
-    ) -> Self {
-        Self::new(message, 400, source)
-    }
-
-    pub fn not_found<S: AsRef<str>>(
-        message: S,
-        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
-    ) -> Self {
-        Self::new(message, 404, source)
-    }
-
-    pub fn internal_server_error<S: AsRef<str>>(
-        message: S,
-        source: Option<Box<dyn StdError + Send + Sync + 'static>>,
-    ) -> Self {
-        Self::new(message, 500, source)
-    }
+#[macro_export]
+macro_rules! http_bail {
+    ($status:ident, $($fmt:tt)+) => {{
+        return Err($crate::api::http_err!($status, $($fmt)+));
+    }};
 }
 
-impl Display for ApiError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(&format!("{} {}", self.code, self.message))
-    }
-}
+pub use http_bail;
+pub use http_err;
 
-impl StdError for ApiError {
-    fn source(&self) -> Option<&(dyn StdError + 'static)> {
-        match &self.source {
-            None => None,
-            Some(source) => Some(&**source),
-        }
-    }
-}
-
-fn verify_digest(config: &Config, digest: Option<&[u8]>) -> Result<(), ApiError> {
+fn verify_digest(config: &Config, digest: Option<&[u8]>) -> Result<(), HttpError> {
     if let Some(digest) = digest {
         if config.digest != *digest {
-            return Err(ApiError::bad_request(
-                "detected modified configuration - file changed by other user? Try again.",
-                None,
-            ));
+            http_bail!(
+                BAD_REQUEST,
+                "detected modified configuration - file changed by other user? Try again."
+            );
         }
     }
 
     Ok(())
 }
 
-fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Result<(), ApiError> {
+fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Result<(), HttpError> {
     #[allow(unused_mut)]
     let mut exists = false;
 
@@ -101,16 +61,16 @@ fn ensure_endpoint_exists(#[allow(unused)] config: &Config, name: &str) -> Resul
     }
 
     if !exists {
-        Err(ApiError::not_found(
-            format!("endpoint '{name}' does not exist"),
-            None,
-        ))
+        http_bail!(NOT_FOUND, "endpoint '{name}' does not exist")
     } else {
         Ok(())
     }
 }
 
-fn ensure_endpoints_exist<T: AsRef<str>>(config: &Config, endpoints: &[T]) -> Result<(), ApiError> {
+fn ensure_endpoints_exist<T: AsRef<str>>(
+    config: &Config,
+    endpoints: &[T],
+) -> Result<(), HttpError> {
     for endpoint in endpoints {
         ensure_endpoint_exists(config, endpoint.as_ref())?;
     }
@@ -118,18 +78,18 @@ fn ensure_endpoints_exist<T: AsRef<str>>(config: &Config, endpoints: &[T]) -> Re
     Ok(())
 }
 
-fn ensure_unique(config: &Config, entity: &str) -> Result<(), ApiError> {
+fn ensure_unique(config: &Config, entity: &str) -> Result<(), HttpError> {
     if config.config.sections.contains_key(entity) {
-        return Err(ApiError::bad_request(
-            format!("Cannot create '{entity}', an entity with the same name already exists"),
-            None,
-        ));
+        http_bail!(
+            BAD_REQUEST,
+            "Cannot create '{entity}', an entity with the same name already exists"
+        );
     }
 
     Ok(())
 }
 
-fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, ApiError> {
+fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, HttpError> {
     let mut referrers = HashSet::new();
 
     for group in group::get_groups(config)? {
@@ -165,16 +125,16 @@ fn get_referrers(config: &Config, entity: &str) -> Result<HashSet<String>, ApiEr
     Ok(referrers)
 }
 
-fn ensure_unused(config: &Config, entity: &str) -> Result<(), ApiError> {
+fn ensure_unused(config: &Config, entity: &str) -> Result<(), HttpError> {
     let referrers = get_referrers(config, entity)?;
 
     if !referrers.is_empty() {
         let used_by = referrers.into_iter().collect::<Vec<_>>().join(", ");
 
-        return Err(ApiError::bad_request(
-            format!("cannot delete '{entity}', referenced by: {used_by}"),
-            None,
-        ));
+        http_bail!(
+            BAD_REQUEST,
+            "cannot delete '{entity}', referenced by: {used_by}"
+        );
     }
 
     Ok(())
@@ -240,7 +200,7 @@ mod tests {
     use crate::filter::FilterConfig;
     use crate::group::GroupConfig;
 
-    fn prepare_config() -> Result<Config, ApiError> {
+    fn prepare_config() -> Result<Config, HttpError> {
         let mut config = super::test_helpers::empty_config();
 
         filter::add_filter(
@@ -316,7 +276,7 @@ mod tests {
     }
 
     #[test]
-    fn test_get_referrers_for_entity() -> Result<(), ApiError> {
+    fn test_get_referrers_for_entity() -> Result<(), HttpError> {
         let config = prepare_config().unwrap();
 
         assert_eq!(
diff --git a/proxmox-notify/src/api/sendmail.rs b/proxmox-notify/src/api/sendmail.rs
index 59a57b4..ac8737c 100644
--- a/proxmox-notify/src/api/sendmail.rs
+++ b/proxmox-notify/src/api/sendmail.rs
@@ -1,4 +1,6 @@
-use crate::api::ApiError;
+use proxmox_http_error::HttpError;
+
+use crate::api::{http_bail, http_err};
 use crate::endpoints::sendmail::{
     DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater, SENDMAIL_TYPENAME,
 };
@@ -7,32 +9,36 @@ use crate::Config;
 /// Get a list of all sendmail endpoints.
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns a list of all sendmail endpoints or an `ApiError` if the config is erroneous.
-pub fn get_endpoints(config: &Config) -> Result<Vec<SendmailConfig>, ApiError> {
+/// Returns a list of all sendmail endpoints or a `HttpError` if the config is
+/// erroneous (`500 Internal server error`).
+pub fn get_endpoints(config: &Config) -> Result<Vec<SendmailConfig>, HttpError> {
     config
         .config
         .convert_to_typed_array(SENDMAIL_TYPENAME)
-        .map_err(|e| ApiError::internal_server_error("Could not fetch endpoints", Some(e.into())))
+        .map_err(|e| http_err!(NOT_FOUND, "Could not fetch endpoints: {e}"))
 }
 
 /// Get sendmail endpoint with given `name`.
 ///
 /// The caller is responsible for any needed permission checks.
-/// Returns the endpoint or an `ApiError` if the endpoint was not found.
-pub fn get_endpoint(config: &Config, name: &str) -> Result<SendmailConfig, ApiError> {
+/// Returns the endpoint or a `HttpError` if the endpoint was not found (`404 Not found`).
+pub fn get_endpoint(config: &Config, name: &str) -> Result<SendmailConfig, HttpError> {
     config
         .config
         .lookup(SENDMAIL_TYPENAME, name)
-        .map_err(|_| ApiError::not_found(format!("endpoint '{name}' not found"), None))
+        .map_err(|_| http_err!(NOT_FOUND, "endpoint '{name}' not found"))
 }
 
 /// Add a new sendmail endpoint.
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if an endpoint with the same name already exists,
-/// or if the endpoint could not be saved.
-pub fn add_endpoint(config: &mut Config, endpoint: &SendmailConfig) -> Result<(), ApiError> {
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+///   - mailto *and* mailto_user are both set to `None`
+pub fn add_endpoint(config: &mut Config, endpoint: &SendmailConfig) -> Result<(), HttpError> {
     super::ensure_unique(config, &endpoint.name)?;
 
     if let Some(filter) = &endpoint.filter {
@@ -41,37 +47,39 @@ pub fn add_endpoint(config: &mut Config, endpoint: &SendmailConfig) -> Result<()
     }
 
     if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
-        return Err(ApiError::bad_request(
-            "must at least provide one recipient, either in mailto or in mailto-user",
-            None,
-        ));
+        http_bail!(
+            BAD_REQUEST,
+            "must at least provide one recipient, either in mailto or in mailto-user"
+        );
     }
 
     config
         .config
         .set_data(&endpoint.name, SENDMAIL_TYPENAME, endpoint)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save endpoint '{}'", endpoint.name),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save endpoint '{}': {e}",
+                endpoint.name
             )
-        })?;
-
-    Ok(())
+        })
 }
 
 /// Update existing sendmail endpoint
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the config could not be saved.
+/// Returns a `HttpError` if:
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+///   - mailto *and* mailto_user are both set to `None`
 pub fn update_endpoint(
     config: &mut Config,
     name: &str,
     updater: &SendmailConfigUpdater,
     delete: Option<&[DeleteableSendmailProperty]>,
     digest: Option<&[u8]>,
-) -> Result<(), ApiError> {
+) -> Result<(), HttpError> {
     super::verify_digest(config, digest)?;
 
     let mut endpoint = get_endpoint(config, name)?;
@@ -115,31 +123,33 @@ pub fn update_endpoint(
     }
 
     if endpoint.mailto.is_none() && endpoint.mailto_user.is_none() {
-        return Err(ApiError::bad_request(
-            "must at least provide one recipient, either in mailto or in mailto-user",
-            None,
-        ));
+        http_bail!(
+            BAD_REQUEST,
+            "must at least provide one recipient, either in mailto or in mailto-user"
+        );
     }
 
     config
         .config
         .set_data(name, SENDMAIL_TYPENAME, &endpoint)
         .map_err(|e| {
-            ApiError::internal_server_error(
-                format!("could not save endpoint '{name}'"),
-                Some(e.into()),
+            http_err!(
+                INTERNAL_SERVER_ERROR,
+                "could not save endpoint '{}': {e}",
+                endpoint.name
             )
-        })?;
-
-    Ok(())
+        })
 }
 
 /// Delete existing sendmail endpoint
 ///
 /// The caller is responsible for any needed permission checks.
 /// The caller also responsible for locking the configuration files.
-/// Returns an `ApiError` if the endpoint does not exist.
-pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), ApiError> {
+/// Returns a `HttpError` if:
+///   - an entity with the same name already exists (`400 Bad request`)
+///   - a referenced filter does not exist (`400 Bad request`)
+///   - the configuration could not be saved (`500 Internal server error`)
+pub fn delete_endpoint(config: &mut Config, name: &str) -> Result<(), HttpError> {
     // Check if the endpoint exists
     let _ = get_endpoint(config, name)?;
     super::ensure_unused(config, name)?;
@@ -154,7 +164,10 @@ pub mod tests {
     use super::*;
     use crate::api::test_helpers::*;
 
-    pub fn add_sendmail_endpoint_for_test(config: &mut Config, name: &str) -> Result<(), ApiError> {
+    pub fn add_sendmail_endpoint_for_test(
+        config: &mut Config,
+        name: &str,
+    ) -> Result<(), HttpError> {
         add_endpoint(
             config,
             &SendmailConfig {
@@ -173,7 +186,7 @@ pub mod tests {
     }
 
     #[test]
-    fn test_sendmail_create() -> Result<(), ApiError> {
+    fn test_sendmail_create() -> Result<(), HttpError> {
         let mut config = empty_config();
 
         assert_eq!(get_endpoints(&config)?.len(), 0);
@@ -186,7 +199,7 @@ pub mod tests {
     }
 
     #[test]
-    fn test_update_not_existing_returns_error() -> Result<(), ApiError> {
+    fn test_update_not_existing_returns_error() -> Result<(), HttpError> {
         let mut config = empty_config();
 
         assert!(update_endpoint(&mut config, "test", &Default::default(), None, None,).is_err());
@@ -195,7 +208,7 @@ pub mod tests {
     }
 
     #[test]
-    fn test_update_invalid_digest_returns_error() -> Result<(), ApiError> {
+    fn test_update_invalid_digest_returns_error() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_sendmail_endpoint_for_test(&mut config, "sendmail-endpoint")?;
 
@@ -219,7 +232,7 @@ pub mod tests {
     }
 
     #[test]
-    fn test_sendmail_update() -> Result<(), ApiError> {
+    fn test_sendmail_update() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_sendmail_endpoint_for_test(&mut config, "sendmail-endpoint")?;
 
@@ -275,7 +288,7 @@ pub mod tests {
     }
 
     #[test]
-    fn test_sendmail_delete() -> Result<(), ApiError> {
+    fn test_sendmail_delete() -> Result<(), HttpError> {
         let mut config = empty_config();
         add_sendmail_endpoint_for_test(&mut config, "sendmail-endpoint")?;
 
-- 
2.39.2





^ permalink raw reply	[flat|nested] 6+ messages in thread

* [pve-devel] [PATCH v2 proxmox-perl-rs 4/4] notify: use new HttpError type
  2023-07-26 14:18 [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Lukas Wagner
                   ` (2 preceding siblings ...)
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 3/4] notify: use HttpError from proxmox-http-error Lukas Wagner
@ 2023-07-26 14:18 ` Lukas Wagner
  2023-07-28  9:58 ` [pve-devel] applied-series: [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Wolfgang Bumiller
  4 siblings, 0 replies; 6+ messages in thread
From: Lukas Wagner @ 2023-07-26 14:18 UTC (permalink / raw)
  To: pve-devel

Use `proxmox-http-error::HttpError` instead of
`proxmox-notify::api::ApiError`.

Also factoring out the digest decoding into a small helper.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 common/src/notify.rs | 77 +++++++++++++++++++++++---------------------
 pve-rs/Cargo.toml    |  1 +
 2 files changed, 42 insertions(+), 36 deletions(-)

diff --git a/common/src/notify.rs b/common/src/notify.rs
index c8ca533..9f44225 100644
--- a/common/src/notify.rs
+++ b/common/src/notify.rs
@@ -5,6 +5,7 @@ mod export {
     use serde_json::Value as JSONValue;
     use std::sync::Mutex;
 
+    use proxmox_http_error::HttpError;
     use proxmox_notify::endpoints::gotify::{
         DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
         GotifyPrivateConfigUpdater,
@@ -16,7 +17,7 @@ mod export {
         DeleteableFilterProperty, FilterConfig, FilterConfigUpdater, FilterModeOperator,
     };
     use proxmox_notify::group::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater};
-    use proxmox_notify::{api, api::ApiError, Config, Notification, Severity};
+    use proxmox_notify::{api, Config, Notification, Severity};
 
     pub struct NotificationConfig {
         config: Mutex<Config>,
@@ -91,7 +92,7 @@ mod export {
         title: String,
         body: String,
         properties: Option<JSONValue>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let config = this.config.lock().unwrap();
 
         let notification = Notification {
@@ -108,13 +109,15 @@ mod export {
     fn test_target(
         #[try_from_ref] this: &NotificationConfig,
         target: &str,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let config = this.config.lock().unwrap();
         api::common::test_target(&config, target)
     }
 
     #[export(serialize_error)]
-    fn get_groups(#[try_from_ref] this: &NotificationConfig) -> Result<Vec<GroupConfig>, ApiError> {
+    fn get_groups(
+        #[try_from_ref] this: &NotificationConfig,
+    ) -> Result<Vec<GroupConfig>, HttpError> {
         let config = this.config.lock().unwrap();
         api::group::get_groups(&config)
     }
@@ -123,7 +126,7 @@ mod export {
     fn get_group(
         #[try_from_ref] this: &NotificationConfig,
         id: &str,
-    ) -> Result<GroupConfig, ApiError> {
+    ) -> Result<GroupConfig, HttpError> {
         let config = this.config.lock().unwrap();
         api::group::get_group(&config, id)
     }
@@ -135,7 +138,7 @@ mod export {
         endpoints: Vec<String>,
         comment: Option<String>,
         filter: Option<String>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::group::add_group(
             &mut config,
@@ -157,11 +160,9 @@ mod export {
         filter: Option<String>,
         delete: Option<Vec<DeleteableGroupProperty>>,
         digest: Option<&str>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
-        let digest = digest.map(hex::decode).transpose().map_err(|e| {
-            ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
-        })?;
+        let digest = decode_digest(digest)?;
 
         api::group::update_group(
             &mut config,
@@ -177,7 +178,10 @@ mod export {
     }
 
     #[export(serialize_error)]
-    fn delete_group(#[try_from_ref] this: &NotificationConfig, name: &str) -> Result<(), ApiError> {
+    fn delete_group(
+        #[try_from_ref] this: &NotificationConfig,
+        name: &str,
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::group::delete_group(&mut config, name)
     }
@@ -185,7 +189,7 @@ mod export {
     #[export(serialize_error)]
     fn get_sendmail_endpoints(
         #[try_from_ref] this: &NotificationConfig,
-    ) -> Result<Vec<SendmailConfig>, ApiError> {
+    ) -> Result<Vec<SendmailConfig>, HttpError> {
         let config = this.config.lock().unwrap();
         api::sendmail::get_endpoints(&config)
     }
@@ -194,7 +198,7 @@ mod export {
     fn get_sendmail_endpoint(
         #[try_from_ref] this: &NotificationConfig,
         id: &str,
-    ) -> Result<SendmailConfig, ApiError> {
+    ) -> Result<SendmailConfig, HttpError> {
         let config = this.config.lock().unwrap();
         api::sendmail::get_endpoint(&config, id)
     }
@@ -210,7 +214,7 @@ mod export {
         author: Option<String>,
         comment: Option<String>,
         filter: Option<String>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
 
         api::sendmail::add_endpoint(
@@ -240,11 +244,9 @@ mod export {
         filter: Option<String>,
         delete: Option<Vec<DeleteableSendmailProperty>>,
         digest: Option<&str>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
-        let digest = digest.map(hex::decode).transpose().map_err(|e| {
-            ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
-        })?;
+        let digest = decode_digest(digest)?;
 
         api::sendmail::update_endpoint(
             &mut config,
@@ -266,7 +268,7 @@ mod export {
     fn delete_sendmail_endpoint(
         #[try_from_ref] this: &NotificationConfig,
         name: &str,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::sendmail::delete_endpoint(&mut config, name)
     }
@@ -274,7 +276,7 @@ mod export {
     #[export(serialize_error)]
     fn get_gotify_endpoints(
         #[try_from_ref] this: &NotificationConfig,
-    ) -> Result<Vec<GotifyConfig>, ApiError> {
+    ) -> Result<Vec<GotifyConfig>, HttpError> {
         let config = this.config.lock().unwrap();
         api::gotify::get_endpoints(&config)
     }
@@ -283,7 +285,7 @@ mod export {
     fn get_gotify_endpoint(
         #[try_from_ref] this: &NotificationConfig,
         id: &str,
-    ) -> Result<GotifyConfig, ApiError> {
+    ) -> Result<GotifyConfig, HttpError> {
         let config = this.config.lock().unwrap();
         api::gotify::get_endpoint(&config, id)
     }
@@ -296,7 +298,7 @@ mod export {
         token: String,
         comment: Option<String>,
         filter: Option<String>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::gotify::add_endpoint(
             &mut config,
@@ -321,11 +323,9 @@ mod export {
         filter: Option<String>,
         delete: Option<Vec<DeleteableGotifyProperty>>,
         digest: Option<&str>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
-        let digest = digest.map(hex::decode).transpose().map_err(|e| {
-            ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
-        })?;
+        let digest = decode_digest(digest)?;
 
         api::gotify::update_endpoint(
             &mut config,
@@ -345,7 +345,7 @@ mod export {
     fn delete_gotify_endpoint(
         #[try_from_ref] this: &NotificationConfig,
         name: &str,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::gotify::delete_gotify_endpoint(&mut config, name)
     }
@@ -353,7 +353,7 @@ mod export {
     #[export(serialize_error)]
     fn get_filters(
         #[try_from_ref] this: &NotificationConfig,
-    ) -> Result<Vec<FilterConfig>, ApiError> {
+    ) -> Result<Vec<FilterConfig>, HttpError> {
         let config = this.config.lock().unwrap();
         api::filter::get_filters(&config)
     }
@@ -362,7 +362,7 @@ mod export {
     fn get_filter(
         #[try_from_ref] this: &NotificationConfig,
         id: &str,
-    ) -> Result<FilterConfig, ApiError> {
+    ) -> Result<FilterConfig, HttpError> {
         let config = this.config.lock().unwrap();
         api::filter::get_filter(&config, id)
     }
@@ -376,7 +376,7 @@ mod export {
         mode: Option<FilterModeOperator>,
         invert_match: Option<bool>,
         comment: Option<String>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::filter::add_filter(
             &mut config,
@@ -401,11 +401,9 @@ mod export {
         comment: Option<String>,
         delete: Option<Vec<DeleteableFilterProperty>>,
         digest: Option<&str>,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
-        let digest = digest.map(hex::decode).transpose().map_err(|e| {
-            ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
-        })?;
+        let digest = decode_digest(digest)?;
 
         api::filter::update_filter(
             &mut config,
@@ -425,7 +423,7 @@ mod export {
     fn delete_filter(
         #[try_from_ref] this: &NotificationConfig,
         name: &str,
-    ) -> Result<(), ApiError> {
+    ) -> Result<(), HttpError> {
         let mut config = this.config.lock().unwrap();
         api::filter::delete_filter(&mut config, name)
     }
@@ -434,8 +432,15 @@ mod export {
     fn get_referenced_entities(
         #[try_from_ref] this: &NotificationConfig,
         name: &str,
-    ) -> Result<Vec<String>, ApiError> {
+    ) -> Result<Vec<String>, HttpError> {
         let config = this.config.lock().unwrap();
         api::common::get_referenced_entities(&config, name)
     }
+
+    fn decode_digest(digest: Option<&str>) -> Result<Option<Vec<u8>>, HttpError> {
+        digest
+            .map(hex::decode)
+            .transpose()
+            .map_err(|e| api::http_err!(BAD_REQUEST, "invalid digest: {e}"))
+    }
 }
diff --git a/pve-rs/Cargo.toml b/pve-rs/Cargo.toml
index 2b375c0..222fdf5 100644
--- a/pve-rs/Cargo.toml
+++ b/pve-rs/Cargo.toml
@@ -35,6 +35,7 @@ perlmod = { version = "0.13", features = [ "exporter" ] }
 
 proxmox-apt = "0.10"
 proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
+proxmox-http-error = "0.1.0"
 proxmox-notify = "0.1"
 proxmox-openid = "0.10"
 proxmox-resource-scheduling = "0.3.0"
-- 
2.39.2





^ permalink raw reply	[flat|nested] 6+ messages in thread

* [pve-devel] applied-series: [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate
  2023-07-26 14:18 [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Lukas Wagner
                   ` (3 preceding siblings ...)
  2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox-perl-rs 4/4] notify: use new HttpError type Lukas Wagner
@ 2023-07-28  9:58 ` Wolfgang Bumiller
  4 siblings, 0 replies; 6+ messages in thread
From: Wolfgang Bumiller @ 2023-07-28  9:58 UTC (permalink / raw)
  To: Lukas Wagner; +Cc: pve-devel

applied series & bumped packages




^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2023-07-28  9:58 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-07-26 14:18 [pve-devel] [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Lukas Wagner
2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 1/4] http-error: add new http-error crate Lukas Wagner
2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 2/4] router: re-export `HttpError` from `proxmox-http-error` Lukas Wagner
2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox 3/4] notify: use HttpError from proxmox-http-error Lukas Wagner
2023-07-26 14:18 ` [pve-devel] [PATCH v2 proxmox-perl-rs 4/4] notify: use new HttpError type Lukas Wagner
2023-07-28  9:58 ` [pve-devel] applied-series: [PATCH v2 proxmox{, -perl-rs} 0/4] move `HttpError` from `proxmox-router` into its own crate Wolfgang Bumiller

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal