Skip to main content

google_cloud_storage/
error.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Custom errors for the Cloud Storage client.
16//!
17//! The storage client defines additional error types. These are often returned
18//! as the `source()` of an [Error][crate::Error].
19
20use crate::model::{Object, ObjectChecksums};
21
22/// Indicates that a checksum mismatch was detected while reading or writing
23/// Cloud Storage object.
24///
25/// When performing a full read of an object, the client library automatically
26/// computes the CRC32C checksum (and optionally the MD5 hash) of the received
27/// data. At the end of the read The client library automatically computes this
28/// checksum to the values reported by the service. If the values do not match,
29/// the read operation completes with an error and the error includes this type
30/// showing which checksums did not match.
31///
32/// Likewise, when performing an object write, the client library automatically
33/// compares the CRC32C checksum (and optionally the MD5 hash) of the data sent
34/// to the service against the values reported by the service when the object is
35/// finalized. If the values do not match, the write operation completes with an
36/// error and the error includes this type.
37#[derive(Clone, Debug)]
38#[non_exhaustive]
39pub enum ChecksumMismatch {
40    /// The CRC32C checksum sent by the service does not match the computed (or expected) value.
41    Crc32c {
42        /// The checksum computed by the client.
43        got: u32,
44        /// The checksum reported by the service.
45        want: u32,
46    },
47
48    /// The MD5 hash sent by the service does not match the computed (or expected) value.
49    Md5 {
50        /// The MD5 hash computed by the client.
51        got: bytes::Bytes,
52        /// The MD5 hash reported by the service.
53        want: bytes::Bytes,
54    },
55
56    /// The CRC32C checksum **and** the MD5 hash sent by the service do not
57    /// match the computed (or expected) values.
58    Both {
59        /// The checksums computed by the client.
60        got: Box<ObjectChecksums>,
61        /// The checksums reported by the service.
62        want: Box<ObjectChecksums>,
63    },
64}
65
66impl std::fmt::Display for ChecksumMismatch {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Crc32c { got, want } => write!(
70                f,
71                "the CRC32C checksums do not match: got=0x{got:08x}, want=0x{want:08x}"
72            ),
73            Self::Md5 { got, want } => write!(
74                f,
75                "the MD5 hashes do not match: got={got:0x?}, want={want:0x?}"
76            ),
77            Self::Both { got, want } => {
78                write!(
79                    f,
80                    "both the CRC32C checksums and MD5 hashes do not match: got.crc32c=0x{:08x}, want.crc32c=0x{:08x}, got.md5={:x?}, want.md5={:x?}",
81                    got.crc32c.unwrap_or_default(),
82                    want.crc32c.unwrap_or_default(),
83                    got.md5_hash,
84                    want.md5_hash
85                )
86            }
87        }
88    }
89}
90
91/// Represents errors that can occur when converting to [KeyAes256] instances.
92///
93/// # Example:
94/// ```
95/// # use google_cloud_storage::{model_ext::KeyAes256, error::KeyAes256Error};
96/// let invalid_key_bytes: &[u8] = b"too_short_key"; // Less than 32 bytes
97/// let result = KeyAes256::new(invalid_key_bytes);
98///
99/// assert!(matches!(result, Err(KeyAes256Error::InvalidLength)));
100/// ```
101///
102/// [KeyAes256]: crate::model_ext::KeyAes256
103#[derive(thiserror::Error, Debug)]
104#[non_exhaustive]
105pub enum KeyAes256Error {
106    /// The provided key's length was not exactly 32 bytes.
107    #[error("Key has an invalid length: expected 32 bytes.")]
108    InvalidLength,
109}
110
111type BoxedSource = Box<dyn std::error::Error + Send + Sync + 'static>;
112
113/// Represents an error that can occur when reading response data.
114#[derive(thiserror::Error, Debug)]
115#[non_exhaustive]
116pub enum ReadError {
117    /// The calculated crc32c did not match server provided crc32c.
118    #[error("checksum mismatch {0}")]
119    ChecksumMismatch(ChecksumMismatch),
120
121    /// The read was interrupted before all the expected bytes arrived.
122    #[error("missing {0} bytes at the end of the stream")]
123    ShortRead(u64),
124
125    /// The read received more bytes than expected.
126    #[error("too many bytes received: expected {expected}, stopped read at {got}")]
127    LongRead {
128        /// The number of bytes actually received.
129        got: u64,
130        /// The number of bytes expected.
131        expected: u64,
132    },
133
134    /// Only 200 and 206 status codes are expected in successful responses.
135    #[error("unexpected success code {0} in read request, only 200 and 206 are expected")]
136    UnexpectedSuccessCode(u16),
137
138    /// Successful HTTP response must include some headers.
139    #[error("the response is missing '{0}', a required header")]
140    MissingHeader(&'static str),
141
142    /// The received header format is invalid.
143    #[error("the format for header '{0}' is incorrect")]
144    BadHeaderFormat(&'static str, #[source] BoxedSource),
145
146    /// A bidi read was interrupted with an unrecoverable error.
147    #[error("cannot recover from an underlying read error: {0}")]
148    UnrecoverableBidiReadInterrupt(#[source] std::sync::Arc<crate::Error>),
149
150    /// A bidi read received an invalid offset.
151    ///
152    /// # Troubleshooting
153    ///
154    /// This indicates a bug in the client, the service, or a message corrupted
155    /// while in transit. Please [open an issue] or contact [Google Cloud support]
156    /// with as much detail as possible.
157    ///
158    /// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
159    /// [Google Cloud support]: https://cloud.google.com/support
160    #[error("the bidi streaming read response is invalid: {0}")]
161    InvalidBidiStreamingReadResponse(#[source] BoxedSource),
162}
163
164impl ReadError {
165    pub(crate) fn bidi_out_of_order(expected: i64, got: i64) -> Self {
166        Self::InvalidBidiStreamingReadResponse(
167            format!("message offset mismatch, expected={expected}, got={got}").into(),
168        )
169    }
170}
171
172/// An unrecoverable problem in the upload protocol.
173///
174/// # Example
175/// ```
176/// # use google_cloud_storage::{client::Storage, error::WriteError};
177/// # async fn sample(client: &Storage) -> anyhow::Result<()> {
178/// use std::error::Error as _;
179/// let writer = client
180///     .write_object("projects/_/buckets/my-bucket", "my-object", "hello world")
181///     .set_if_generation_not_match(0);
182/// match writer.send_buffered().await {
183///     Ok(object) => println!("Successfully created the object {object:?}"),
184///     Err(error) if error.is_serialization() => {
185///         println!("Some problem {error:?} sending the data to the service");
186///         if let Some(m) = error.source().and_then(|e| e.downcast_ref::<WriteError>()) {
187///             println!("{m}");
188///         }
189///     },
190///     Err(e) => return Err(e.into()), // not handled in this example
191/// }
192/// # Ok(()) }
193/// ```
194///
195#[derive(thiserror::Error, Debug)]
196#[non_exhaustive]
197pub enum WriteError {
198    /// The service has "uncommitted" previously persisted bytes.
199    ///
200    /// # Troubleshoot
201    ///
202    /// In the resumable upload protocol the service reports how many bytes are
203    /// persisted. This error indicates that the service previously reported
204    /// more bytes as persisted than in the latest report. This could indicate:
205    /// - a corrupted message from the service, either the earlier message
206    ///   reporting more bytes persisted than actually are, or the current
207    ///   message indicating fewer bytes persisted.
208    /// - a bug in the service, where it reported bytes as persisted when they
209    ///   were not.
210    /// - a bug in the client, maybe storing the incorrect byte count, or
211    ///   parsing the messages incorrectly.
212    ///
213    /// All of these conditions indicate a bug, and in Rust it is idiomatic to
214    /// `panic!()` when a bug is detected. However, in this case it seems more
215    /// appropriate to report the problem, as the client library cannot
216    /// determine the location of the bug.
217    #[error(
218        "the service previously persisted {offset} bytes, but now reports only {persisted} as persisted"
219    )]
220    UnexpectedRewind {
221        /// The offset reported previously.
222        offset: u64,
223        /// The offset reported in the current message.
224        persisted: u64,
225    },
226
227    /// The service reports more bytes persisted than sent.
228    ///
229    /// # Troubleshoot
230    ///
231    /// Most likely this indicates that two concurrent uploads are using the
232    /// same session. Review your application design to avoid concurrent
233    /// uploads.
234    ///
235    /// It is possible that this indicates a bug in the service, client, or
236    /// messages corrupted in transit.
237    #[error("the service reports {persisted} bytes as persisted, but we only sent {sent} bytes")]
238    TooMuchProgress {
239        /// The number of bytes sent by the client.
240        sent: u64,
241        /// The number of bytes reported as persisted by the service.
242        persisted: u64,
243    },
244
245    /// The checksums reported by the service do not match the expected checksums.
246    ///
247    /// # Troubleshoot
248    ///
249    /// The client library compares the CRC32C checksum and/or MD5 hash of the
250    /// uploaded data against the hash reported by the service at the end of
251    /// the upload. This error indicates the hashes did not match.
252    ///
253    /// If you provided known values for these checksums verify those values are
254    /// correct.
255    ///
256    /// Otherwise, this is probably a data corruption problem. These are
257    /// notoriously difficult to root cause. They probably indicate faulty
258    /// equipment, such as the physical machine hosting your client, the network
259    /// elements between your client and the service, or the physical machine
260    /// hosting the service.
261    ///
262    /// If possible, resend the data from a different machine.
263    #[error("checksum mismatch {mismatch} when uploading {} to {}", object.name, object.bucket)]
264    ChecksumMismatch {
265        /// The details of the checksum mismatch.
266        mismatch: ChecksumMismatch,
267        /// The object metadata.
268        object: Box<Object>,
269    },
270}
271
272type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
273
274/// Signed URL creation errors.
275#[derive(thiserror::Error, Debug)]
276#[error(transparent)]
277pub struct SigningError(SigningErrorKind);
278
279impl SigningError {
280    /// Returns true if the error was due to a problem signing the URL.
281    pub fn is_signing(&self) -> bool {
282        matches!(self.0, SigningErrorKind::Signing(_))
283    }
284
285    /// Returns true if the error was due to an invalid parameter.
286    pub fn is_invalid_parameter(&self) -> bool {
287        matches!(self.0, SigningErrorKind::InvalidParameter(_, _))
288    }
289
290    /// A problem to sign the URL.
291    pub(crate) fn signing<T>(source: T) -> SigningError
292    where
293        T: Into<BoxError>,
294    {
295        SigningError(SigningErrorKind::Signing(source.into()))
296    }
297
298    /// A problem to sign the URL due to invalid input.
299    pub(crate) fn invalid_parameter<S: Into<String>, T>(field: S, source: T) -> SigningError
300    where
301        T: Into<BoxError>,
302    {
303        SigningError(SigningErrorKind::InvalidParameter(
304            field.into(),
305            source.into(),
306        ))
307    }
308}
309
310#[derive(thiserror::Error, Debug)]
311#[allow(dead_code)]
312enum SigningErrorKind {
313    /// The signing operation failed.
314    #[error("signing failed: {0}")]
315    Signing(#[source] BoxError),
316
317    /// An invalid input was provided to generate a signed URL.
318    #[error("invalid `{0}` parameter: {1}")]
319    InvalidParameter(String, #[source] BoxError),
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn mismatch_crc32c() {
328        let value = ChecksumMismatch::Crc32c {
329            got: 0x01020304_u32,
330            want: 0x02030405_u32,
331        };
332        let fmt = value.to_string();
333        assert!(fmt.contains("got=0x01020304"), "{value:?} => {fmt}");
334        assert!(fmt.contains("want=0x02030405"), "{value:?} => {fmt}");
335    }
336
337    #[test]
338    fn mismatch_md5() {
339        let value = ChecksumMismatch::Md5 {
340            got: bytes::Bytes::from_owner([0x01_u8, 0x02, 0x03, 0x04]),
341            want: bytes::Bytes::from_owner([0x02_u8, 0x03, 0x04, 0x05]),
342        };
343        let fmt = value.to_string();
344        assert!(
345            fmt.contains(r#"got=b"\x01\x02\x03\x04""#),
346            "{value:?} => {fmt}"
347        );
348        assert!(
349            fmt.contains(r#"want=b"\x02\x03\x04\x05""#),
350            "{value:?} => {fmt}"
351        );
352    }
353
354    #[test]
355    fn mismatch_both() {
356        let got = ObjectChecksums::new()
357            .set_crc32c(0x01020304_u32)
358            .set_md5_hash(bytes::Bytes::from_owner([0x01_u8, 0x02, 0x03, 0x04]));
359        let want = ObjectChecksums::new()
360            .set_crc32c(0x02030405_u32)
361            .set_md5_hash(bytes::Bytes::from_owner([0x02_u8, 0x03, 0x04, 0x05]));
362        let value = ChecksumMismatch::Both {
363            got: Box::new(got),
364            want: Box::new(want),
365        };
366        let fmt = value.to_string();
367        assert!(fmt.contains("got.crc32c=0x01020304"), "{value:?} => {fmt}");
368        assert!(fmt.contains("want.crc32c=0x02030405"), "{value:?} => {fmt}");
369        assert!(
370            fmt.contains(r#"got.md5=b"\x01\x02\x03\x04""#),
371            "{value:?} => {fmt}"
372        );
373        assert!(
374            fmt.contains(r#"want.md5=b"\x02\x03\x04\x05""#),
375            "{value:?} => {fmt}"
376        );
377    }
378
379    #[test]
380    fn signing_errors() {
381        let value = SigningError::signing("sign error".to_string());
382        let fmt = value.to_string();
383        assert!(
384            fmt.contains("signing failed: sign error"),
385            "{value:?} => {fmt}"
386        );
387
388        let value = SigningError::invalid_parameter("endpoint", "missing scheme".to_string());
389        let fmt = value.to_string();
390        assert!(
391            fmt.contains("invalid `endpoint` parameter: missing scheme"),
392            "{value:?} => {fmt}"
393        );
394    }
395}