b2_client/
error.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2   License, v. 2.0. If a copy of the MPL was not distributed with this
3   file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6//! Error types for b2-client
7//!
8//! Errors are divided into two types:
9//!
10//! * [ValidationError] for data validation errors prior to sending a request to
11//!   the B2 API. This is currently being split into multiple errors as
12//!   appropriate:
13//!     * [BadHeaderName]
14//!     * [BucketValidationError]
15//!     * [CorsRuleValidationError]
16//!     * [FileNameValidationError]
17//!     * [LifecycleRuleValidationError]
18//!     * [MissingData]
19//! * [Error] for errors returned by the Backblaze B2 API or the HTTP client.
20
21use std::{
22    collections::HashMap,
23    fmt,
24};
25
26use crate::bucket::LifecycleRule;
27
28use serde::{Serialize, Deserialize};
29
30
31// TODO: Splitting these up will provide nicer error handling when the user
32// actually wants to handle them, rather than merely print them. As part of
33// this, we need data-oriented errors rather than string-oriented errors.
34/// Errors from validating B2 requests prior to making the request.
35#[derive(Debug)]
36pub enum ValidationError {
37    /// Failure to parse a URL.
38    ///
39    /// The string the problematic URL.
40    BadUrl(String),
41    /// The data is an invalid format or contains invalid information.
42    ///
43    /// The string is a short description of the failure.
44    BadFormat(String),
45    /// Required information was not provided.
46    ///
47    /// The string is a short description of the failure.
48    MissingData(String),
49    /// The data is outside its valid range.
50    ///
51    /// The string is a short description of the failure.
52    OutOfBounds(String),
53    /// Two pieces of data are incompatible together.
54    ///
55    /// The string is a short description of the failure.
56    Incompatible(String),
57}
58
59impl std::error::Error for ValidationError {}
60
61impl fmt::Display for ValidationError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::BadUrl(s) => write!(f, "Error parsing URL: {}", s),
65            Self::BadFormat(s) => write!(f, "{}", s),
66            Self::MissingData(s) => write!(f, "{}", s),
67            Self::OutOfBounds(s) => write!(f, "{}", s),
68            Self::Incompatible(s) => write!(f, "{}", s),
69        }
70    }
71}
72
73impl From<url::ParseError> for ValidationError {
74    fn from(e: url::ParseError) -> Self {
75        Self::BadUrl(format!("{}", e))
76    }
77}
78
79#[cfg(feature = "with_surf")]
80impl From<http_types::Error> for ValidationError {
81    fn from(e: http_types::Error) -> Self {
82        Self::BadFormat(format!("{}", e))
83    }
84}
85
86#[cfg(feature = "with_hyper")]
87impl From<hyper::header::InvalidHeaderName> for ValidationError {
88    fn from(e: hyper::header::InvalidHeaderName) -> Self {
89        Self::BadFormat(format!("{}", e))
90    }
91}
92
93#[cfg(feature = "with_hyper")]
94impl From<hyper::header::InvalidHeaderValue> for ValidationError {
95    fn from(e: hyper::header::InvalidHeaderValue) -> Self {
96        Self::BadFormat(format!("{}", e))
97    }
98}
99
100#[cfg(feature = "with_isahc")]
101impl From<isahc::http::header::InvalidHeaderName> for ValidationError {
102    fn from(e: isahc::http::header::InvalidHeaderName) -> Self {
103        Self::BadFormat(format!("{}", e))
104    }
105}
106
107#[cfg(feature = "with_isahc")]
108impl From<isahc::http::header::InvalidHeaderValue> for ValidationError {
109    fn from(e: isahc::http::header::InvalidHeaderValue) -> Self {
110        Self::BadFormat(format!("{}", e))
111    }
112}
113
114/// Generic invalid data error.
115#[derive(Debug)]
116pub struct BadData<T>
117    where T: std::fmt::Debug + std::fmt::Display,
118{
119    pub value: T,
120    pub(crate) msg: String,
121}
122
123impl<T> std::error::Error for BadData<T>
124    where T: std::fmt::Debug + std::fmt::Display,
125{}
126
127impl<T> fmt::Display for BadData<T>
128    where T: std::fmt::Debug + std::fmt::Display,
129{
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "{}", self.msg)
132    }
133}
134
135/// Error type with information on invalid HTTP header name.
136#[derive(Debug)]
137pub struct BadHeaderName {
138    /// The name of the bad header.
139    pub header: String,
140    /// The illegal character in the header name.
141    pub invalid_char: char,
142}
143
144impl std::error::Error for BadHeaderName {}
145
146impl fmt::Display for BadHeaderName {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        write!(f, "Invalid character in header {}: {}",
149            self.header,
150            self.invalid_char
151        )
152    }
153}
154
155/// Error type for invalid bucket names.
156#[derive(Debug)]
157pub enum BucketValidationError {
158    /// The name of the bucket must be between 8 and 50 characters, inclusive.
159    // TODO: find full B2 requirement - bytes? ASCII-only characters? I think it
160    // will be the latter since only ASCII control characters were explicitly
161    // prohibited.
162    BadNameLength(usize),
163    /// Illegal character in filename.
164    InvalidChar(char),
165}
166
167impl std::error::Error for BucketValidationError {}
168
169impl fmt::Display for BucketValidationError {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::BadNameLength(sz) => write!(f,
173                "Name must be between 6 and 50 characters, inclusive. Was {}",
174                sz
175            ),
176            Self::InvalidChar(ch) => write!(f, "Unexpected character: {}", ch),
177        }
178    }
179}
180
181// TODO: Likely need to copy-paste this since the compiler won't reliably use
182// the new name (same for `use as`).
183/// Error type for invalid CORS rule names.
184///
185/// The requirements for CORS rules are the same as for bucket names.
186pub type CorsRuleValidationError = BucketValidationError;
187
188/// Error type for bad filenames.
189#[derive(Debug)]
190pub enum FileNameValidationError {
191    /// A filename length cannot exceed 1024 bytes.
192    BadLength(usize),
193    /// An invalid character was in the filename string.
194    InvalidChar(char),
195}
196
197impl std::error::Error for FileNameValidationError {}
198
199impl fmt::Display for FileNameValidationError {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match self {
202            Self::BadLength(sz) => write!(f,
203                "Name must be no more than 1024 bytes. Was {}", sz
204            ),
205            Self::InvalidChar(ch) => write!(f, "Illegal character: {}", ch),
206        }
207    }
208}
209
210/// Error type from failure to validate a set of [LifecycleRule]s.
211#[derive(Debug)]
212pub enum LifecycleRuleValidationError {
213    /// The maximum number of rules (100) was exceeded for the bucket.
214    TooManyRules(usize),
215    /// Multiple [LifecycleRule]s exist for the same file.
216    ///
217    /// Returns a map of conflicting filename prefixes; the most broad prefix
218    /// (the base path) for each group of conflicts is the key and the
219    /// conflicting rules are in the value.
220    ///
221    /// There can be duplicate entries in the map when rules involving
222    /// subfolders exist.
223    ConflictingRules(HashMap<String, Vec<LifecycleRule>>),
224}
225
226impl std::error::Error for LifecycleRuleValidationError {}
227
228impl fmt::Display for LifecycleRuleValidationError {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            Self::TooManyRules(i) => write!(f, concat!(
232                "A bucket can have no more than 100 rules;",
233                "you have provided {}"), i
234            ),
235            Self::ConflictingRules(_) => (write!(f,
236                "Only one lifecycle rule can apply to any given set of files"
237            )),
238        }
239    }
240}
241
242#[derive(Debug)]
243pub struct MissingData {
244    pub field: &'static str,
245    msg: Option<String>,
246}
247
248impl MissingData {
249    pub fn new(missing_field: &'static str) -> Self {
250        Self {
251            field: missing_field,
252            msg: None,
253        }
254    }
255
256    pub fn with_message(mut self, message: impl Into<String>) -> Self {
257        self.msg = Some(message.into());
258        self
259    }
260}
261
262impl std::error::Error for MissingData {}
263
264impl fmt::Display for MissingData {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match &self.msg {
267            Some(msg) => write!(f, "{}", msg),
268            None => write!(f, "Missing data: {} is required", self.field),
269        }
270    }
271}
272
273/// Errors related to making B2 API calls.
274#[derive(Debug)]
275pub enum Error<E>
276    // Surf's Error doesn't implement StdError.
277    where E: fmt::Debug + fmt::Display,
278{
279    /// An error from the underlying HTTP client.
280    Client(E),
281    /// An I/O error from the local filesystem.
282    IO(std::io::Error),
283    /// An error from the Backblaze B2 API.
284    B2(B2Error),
285    /// An error deserializing the HTTP client's response.
286    Format(serde_json::Error),
287    /// The [Authorization](crate::account::Authorization) lacks a required
288    /// capability to perform a task. The provided capability is required.
289    ///
290    /// This error is typically used when the B2 API returns `null` rather than
291    /// returning an error or to return what we know will be an authorization
292    /// error prior to sending a request to the API.
293    Unauthorized(crate::account::Capability),
294    /// An error validating data prior to making a Backblaze B2 API call.
295    Validation(ValidationError),
296    /// Attempted to send a request without a valid
297    /// [Authorization](crate::account::Authorization).
298    MissingAuthorization,
299    /// Attempted to send a non-existent request.
300    NoRequest,
301}
302
303impl<E> std::error::Error for Error<E>
304    where E: fmt::Debug + fmt::Display,
305{}
306
307impl<E> fmt::Display for Error<E>
308    where E: fmt::Debug + fmt::Display,
309{
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        use fmt::Display;
312
313        match self {
314            Self::Client(e) => Display::fmt(&e, f),
315            Self::IO(e) => e.fmt(f),
316            Self::B2(e) => Display::fmt(&e, f),
317            Self::Format(e) => e.fmt(f),
318            Self::Unauthorized(c) => write!(f, "Missing capability: {:?}", c),
319            Self::Validation(e) => e.fmt(f),
320            Self::MissingAuthorization =>
321                write!(f, "An Authorization is required for that operation"),
322            Self::NoRequest => write!(f, "No request was created"),
323        }
324    }
325}
326
327impl<E> From<B2Error> for Error<E>
328    where E: fmt::Debug + fmt::Display,
329{
330    fn from(e: B2Error) -> Self {
331        Self::B2(e)
332    }
333}
334
335impl<E> From<std::io::Error> for Error<E>
336    where E: fmt::Debug + fmt::Display,
337{
338    fn from(e: std::io::Error) -> Self {
339        Self::IO(e)
340    }
341}
342
343impl<E> From<serde_json::Error> for Error<E>
344    where E: fmt::Debug + fmt::Display,
345{
346    fn from(e: serde_json::Error) -> Self {
347        Self::Format(e)
348    }
349}
350
351#[cfg(feature = "with_surf")]
352impl From<surf::Error> for Error<surf::Error> {
353    fn from(e: surf::Error) -> Self {
354        Self::Client(e)
355    }
356}
357
358#[cfg(feature = "with_hyper")]
359impl From<hyper::Error> for Error<hyper::Error> {
360    fn from(e: hyper::Error) -> Self {
361        Self::Client(e)
362    }
363}
364
365#[cfg(feature = "with_isahc")]
366impl From<isahc::Error> for Error<isahc::Error> {
367    fn from(e: isahc::Error) -> Self {
368        Self::Client(e)
369    }
370}
371
372#[cfg(feature = "with_isahc")]
373impl From<isahc::http::Error> for Error<isahc::Error> {
374    fn from(e: isahc::http::Error) -> Self {
375        Self::Client(e.into())
376    }
377}
378
379impl<E> From<ValidationError> for Error<E>
380    where E: fmt::Debug + fmt::Display,
381{
382    fn from(e: ValidationError) -> Self {
383        Self::Validation(e)
384    }
385}
386
387/// An error code from the B2 API.
388///
389/// The HTTP status code is not necessarily constant for any given error code.
390/// See the B2 documentation for the relevant API call to match HTTP status
391/// codes and B2 error codes.
392#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
393pub enum ErrorCode {
394    // 400
395    BadBucketId,
396    BadRequest, // Also 503
397    BucketMissingFileLock,
398    DuplicateBucketName,
399    FileNotPresent,
400    InvalidBucketId,
401    InvalidFileId,
402    NoSuchFile,
403    // The only documented receiving of this isn't possible for us unless B2
404    // modifies the range because we clamp the user's value to the valid range.
405    OutOfRange,
406    TooManyBuckets,
407
408    // 401
409    AccessDenied, // Also 403
410    BadAuthToken,
411    ExpiredAuthToken,
412    Unauthorized,
413    Unsupported,
414
415    // 403
416    CapExceeded,
417    StorageCapExceeded,
418    TransactionCapExceeded,
419
420    // 404
421    NotFound,
422
423    // 405
424    MethodNotAllowed,
425
426    // 408
427    RequestTimeout,
428
429    // 409
430    Conflict,
431
432    // 416
433    RangeNotSatisfiable,
434
435    // 500
436    InternalError,
437
438    // 503
439    ServiceUnavailable,
440
441    /// The B2 service returned an unknown error code.
442    ///
443    /// If you receive this in practice, please file an issue or submit a patch
444    /// to support the error code.
445    Unknown(String),
446}
447
448impl ErrorCode {
449    /// Convert the error code received from the B2 service to an [ErrorCode].
450    fn from_api_code<S: AsRef<str>>(code: S) -> Self {
451        match code.as_ref() {
452            "bad_bucket_id" => Self::BadBucketId,
453            "bad_request" => Self::BadRequest,
454            "bucket_missing_file_lock" => Self::BucketMissingFileLock,
455            "duplicate_bucket_name" => Self::DuplicateBucketName,
456            "file_not_present" => Self::FileNotPresent,
457            "invalid_bucket_id" => Self::InvalidBucketId,
458            "invalid_file_id" => Self::InvalidFileId,
459            "no_such_file" => Self::NoSuchFile,
460            "out_of_range" => Self::OutOfRange,
461            "too_many_buckets" => Self::TooManyBuckets,
462
463            "bad_auth_token" => Self::BadAuthToken,
464            "expired_auth_token" => Self::ExpiredAuthToken,
465            "unauthorized" => Self::Unauthorized,
466            "unsupported" => Self::Unsupported,
467
468            "access_denied" => Self::AccessDenied,
469            "cap_exceeded" => Self::CapExceeded,
470            "storage_cap_exceeded" => Self::StorageCapExceeded,
471            "transaction_cap_exceeded" => Self::TransactionCapExceeded,
472
473            "not_found" => Self::NotFound,
474
475            "method_not_allowed" => Self::MethodNotAllowed,
476
477            "request_timeout" => Self::RequestTimeout,
478
479            "range_not_satisfiable" => Self::RangeNotSatisfiable,
480
481            "conflict" => Self::Conflict,
482
483            "internal_error" => Self::InternalError,
484
485            "service_unavailable" => Self::ServiceUnavailable,
486
487            s => Self::Unknown(s.to_owned()),
488        }
489    }
490}
491
492/// An error response from the Backblaze B2 API.
493///
494/// See <https://www.backblaze.com/b2/docs/calling.html#error_handling> for
495/// information on B2 error handling.
496#[derive(Debug, Deserialize)]
497pub struct B2Error {
498    /// The HTTP status code accompanying the error.
499    status: u16,
500    /// A code that identifies the error.
501    #[serde(rename = "code")]
502    code_str: String,
503    /// A description of what went wrong.
504    message: String,
505}
506
507impl B2Error {
508    /// Get the HTTP status code for the error.
509    pub fn http_status(&self) -> u16 { self.status }
510
511    pub fn code(&self) -> ErrorCode {
512        ErrorCode::from_api_code(&self.code_str)
513    }
514}
515
516impl std::error::Error for B2Error {}
517
518impl fmt::Display for B2Error {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        write!(f, "{}: {}", self.code_str, self.message)
521    }
522}