#![deny(missing_docs)]
use reqwest::StatusCode;
use snafu::Snafu;
const SC_UNAUTHORIZED: u16 = StatusCode::UNAUTHORIZED.as_u16();
const SC_FORBIDDEN: u16 = StatusCode::FORBIDDEN.as_u16();
const SC_NOT_FOUND: u16 = StatusCode::NOT_FOUND.as_u16();
const SC_CONFLICT: u16 = StatusCode::CONFLICT.as_u16();
const SC_REQUEST_TIMEOUT: u16 = StatusCode::REQUEST_TIMEOUT.as_u16();
const SC_TOO_MANY_REQUESTS: u16 = StatusCode::TOO_MANY_REQUESTS.as_u16();
const SC_SERVER_ERROR_RANGE: std::ops::RangeInclusive<u16> = 500..=599;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
#[non_exhaustive]
pub enum NifiError {
#[snafu(display("HTTP request failed: {source}"))]
Http {
source: reqwest::Error,
},
#[snafu(display("Failed to parse NiFi base URL: {source}"))]
InvalidBaseUrl {
source: url::ParseError,
},
#[snafu(display("Authentication failed: {message}"))]
Auth {
message: String,
},
#[snafu(display("Invalid CA certificate: {source}"))]
InvalidCertificate {
source: reqwest::Error,
},
#[snafu(display("Unauthorized (401): {message}"))]
Unauthorized {
message: String,
},
#[snafu(display("Forbidden (403): {message}"))]
Forbidden {
message: String,
},
#[snafu(display("Not found (404): {message}"))]
NotFound {
message: String,
},
#[snafu(display("Conflict (409): {message}"))]
Conflict {
message: String,
},
#[snafu(display("NiFi API error (status {status}): {message}"))]
Api {
status: u16,
message: String,
},
#[snafu(display("NiFi version {detected} is not supported by this client build"))]
UnsupportedVersion {
detected: String,
},
#[snafu(display("Endpoint {endpoint} is not available in NiFi {version}"))]
UnsupportedEndpoint {
endpoint: String,
version: String,
},
#[snafu(display(
"Enum variant '{variant}' of type '{type_name}' is not supported in NiFi {version}"
))]
UnsupportedEnumVariant {
variant: String,
type_name: String,
version: String,
},
#[snafu(display(
"Query parameter '{param}' on endpoint '{endpoint}' is not supported in NiFi {detected_version} (supported in: {supported_in:?})"
))]
UnsupportedQueryParam {
endpoint: &'static str,
param: &'static str,
detected_version: String,
supported_in: Vec<String>,
},
#[snafu(display("required field `{path}` was not populated"))]
MissingField {
path: String,
},
#[snafu(display("NiFi operation {operation} timed out"))]
Timeout {
operation: String,
},
}
impl NifiError {
pub fn status_code(&self) -> Option<u16> {
match self {
Self::Unauthorized { .. } => Some(SC_UNAUTHORIZED),
Self::Forbidden { .. } => Some(SC_FORBIDDEN),
Self::NotFound { .. } => Some(SC_NOT_FOUND),
Self::Conflict { .. } => Some(SC_CONFLICT),
Self::Api { status, .. } => Some(*status),
_ => None,
}
}
pub fn is_retryable(&self) -> bool {
match self {
Self::Http { .. } => true,
Self::Api { status, .. } => {
*status == SC_REQUEST_TIMEOUT
|| *status == SC_TOO_MANY_REQUESTS
|| SC_SERVER_ERROR_RANGE.contains(status)
}
Self::Timeout { .. } => false,
_ => false,
}
}
}
pub(crate) fn api_error(status: u16, message: String) -> NifiError {
match status {
SC_UNAUTHORIZED => NifiError::Unauthorized { message },
SC_FORBIDDEN => NifiError::Forbidden { message },
SC_NOT_FOUND => NifiError::NotFound { message },
SC_CONFLICT => NifiError::Conflict { message },
_ => NifiError::Api { status, message },
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_field_variant_exists_and_is_not_retryable() {
let err = NifiError::MissingField {
path: "about.version".to_string(),
};
assert!(!err.is_retryable());
assert_eq!(err.status_code(), None);
assert_eq!(
err.to_string(),
"required field `about.version` was not populated"
);
}
#[test]
fn unsupported_query_param_variant_renders() {
let err = NifiError::UnsupportedQueryParam {
endpoint: "GET /flow/metrics/{producer}",
param: "registries",
detected_version: "2.6.0".to_string(),
supported_in: vec!["2.8.0".to_string(), "2.9.0".to_string()],
};
assert!(!err.is_retryable());
assert_eq!(err.status_code(), None);
let msg = err.to_string();
assert!(msg.contains("registries"));
assert!(msg.contains("2.6.0"));
}
}