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 { got: u32, want: u32 },
42
43    /// The MD5 hash sent by the service does not match the computed (or expected) value.
44    Md5 {
45        got: bytes::Bytes,
46        want: bytes::Bytes,
47    },
48
49    /// The CRC32C checksum **and** the MD5 hash sent by the service do not
50    /// match the computed (or expected) values.
51    Both {
52        got: Box<ObjectChecksums>,
53        want: Box<ObjectChecksums>,
54    },
55}
56
57impl std::fmt::Display for ChecksumMismatch {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            Self::Crc32c { got, want } => write!(
61                f,
62                "the CRC32C checksums do not match: got=0x{got:08x}, want=0x{want:08x}"
63            ),
64            Self::Md5 { got, want } => write!(
65                f,
66                "the MD5 hashes do not match: got={:0x?}, want={:0x?}",
67                &got, &want
68            ),
69            Self::Both { got, want } => {
70                write!(
71                    f,
72                    "both the CRC32C checksums and MD5 hashes do not match: got.crc32c=0x{:08x}, want.crc32c=0x{:08x}, got.md5={:x?}, want.md5={:x?}",
73                    got.crc32c.unwrap_or_default(),
74                    want.crc32c.unwrap_or_default(),
75                    got.md5_hash,
76                    want.md5_hash
77                )
78            }
79        }
80    }
81}
82
83/// Represents errors that can occur when converting to [KeyAes256] instances.
84///
85/// # Example:
86/// ```
87/// # use google_cloud_storage::{model_ext::KeyAes256, error::KeyAes256Error};
88/// let invalid_key_bytes: &[u8] = b"too_short_key"; // Less than 32 bytes
89/// let result = KeyAes256::new(invalid_key_bytes);
90///
91/// assert!(matches!(result, Err(KeyAes256Error::InvalidLength)));
92/// ```
93///
94/// [KeyAes256]: crate::model_ext::KeyAes256
95#[derive(thiserror::Error, Debug)]
96#[non_exhaustive]
97pub enum KeyAes256Error {
98    /// The provided key's length was not exactly 32 bytes.
99    #[error("Key has an invalid length: expected 32 bytes.")]
100    InvalidLength,
101}
102
103/// Represents an error that can occur when reading response data.
104#[derive(thiserror::Error, Debug)]
105#[non_exhaustive]
106pub enum ReadError {
107    /// The calculated crc32c did not match server provided crc32c.
108    #[error("checksum mismatch {0}")]
109    ChecksumMismatch(ChecksumMismatch),
110
111    /// The read was interrupted before all the expected bytes arrived.
112    #[error("missing {0} bytes at the end of the stream")]
113    ShortRead(u64),
114
115    /// The read received more bytes than expected.
116    #[error("too many bytes received: expected {expected}, stopped read at {got}")]
117    LongRead { got: u64, expected: u64 },
118
119    /// Only 200 and 206 status codes are expected in successful responses.
120    #[error("unexpected success code {0} in read request, only 200 and 206 are expected")]
121    UnexpectedSuccessCode(u16),
122
123    /// Successful HTTP response must include some headers.
124    #[error("the response is missing '{0}', a required header")]
125    MissingHeader(&'static str),
126
127    /// The received header format is invalid.
128    #[error("the format for header '{0}' is incorrect")]
129    BadHeaderFormat(
130        &'static str,
131        #[source] Box<dyn std::error::Error + Send + Sync + 'static>,
132    ),
133
134    /// A bidi read was interrupted with an unrecoverable error.
135    #[cfg(google_cloud_unstable_storage_bidi)]
136    #[error("cannot recover from an underlying read error: {0}")]
137    UnrecoverableBidiReadInterrupt(#[source] std::sync::Arc<crate::Error>),
138
139    /// A bidi read received an invalid offset.
140    ///
141    /// # Troubleshooting
142    ///
143    /// This indicates a bug in the service or a corrupted message in
144    /// transit Please contact [Google Cloud support] with as much detail as
145    /// possible.
146    ///
147    /// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
148    /// [Google Cloud support]: https://cloud.google.com/support
149    #[cfg(google_cloud_unstable_storage_bidi)]
150    #[error("invalid offset in bidi response: {0}")]
151    BadOffsetInBidiResponse(i64),
152
153    /// A bidi read received an invalid length.
154    ///
155    /// # Troubleshooting
156    ///
157    /// This indicates a bug in the service or a corrupted message in
158    /// transit. Please contact [Google Cloud support] with as much detail as
159    /// possible.
160    ///
161    /// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
162    /// [Google Cloud support]: https://cloud.google.com/support
163    #[cfg(google_cloud_unstable_storage_bidi)]
164    #[error("invalid length in bidi response: {0}")]
165    BadLengthInBidiResponse(i64),
166
167    /// A bidi read without a valid range.
168    ///
169    /// # Troubleshooting
170    ///
171    /// This indicates a bug in the service or a corrupted message in
172    /// transit Please contact [Google Cloud support] with as much detail as
173    /// possible.
174    ///
175    /// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
176    /// [Google Cloud support]: https://cloud.google.com/support
177    #[cfg(google_cloud_unstable_storage_bidi)]
178    #[error("missing range in bidi response")]
179    MissingRangeInBidiResponse,
180
181    /// An out of order bidi read.
182    ///
183    /// # Troubleshooting
184    ///
185    /// The client library received an out-of-sequence range of data. This
186    /// indicates a bug in the service or the client library.
187    ///
188    /// Please [open an issue] with as much detail as possible or contact
189    /// [Google Cloud support].
190    ///
191    /// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
192    /// [Google Cloud support]: https://cloud.google.com/support
193    #[cfg(google_cloud_unstable_storage_bidi)]
194    #[error("out of order bidi response, expected offset={expected}, got={got}")]
195    OutOfOrderBidiResponse { got: i64, expected: i64 },
196
197    /// The service returned a range id unknown to the client library.
198    ///
199    /// # Troubleshooting
200    ///
201    /// In bidi reads the application may issue multiple concurrent reads for
202    /// different ranges of the same object. The client library assigns ids to
203    /// each range. This indicates a bug in the service or the client library.
204    ///
205    /// Please [open an issue] with as much detail as possible or contact
206    /// [Google Cloud support].
207    ///
208    /// [open an issue]: https://github.com/googleapis/google-cloud-rust/issues/new/choose
209    /// [Google Cloud support]: https://cloud.google.com/support
210    #[cfg(google_cloud_unstable_storage_bidi)]
211    #[error("unknown range id in bidi response: {0}")]
212    UnknownBidiRangeId(i64),
213}
214
215/// An unrecoverable problem in the upload protocol.
216///
217/// # Example
218/// ```
219/// # use google_cloud_storage::{client::Storage, error::WriteError};
220/// # async fn sample(client: &Storage) -> anyhow::Result<()> {
221/// use std::error::Error as _;
222/// let writer = client
223///     .write_object("projects/_/buckets/my-bucket", "my-object", "hello world")
224///     .set_if_generation_not_match(0);
225/// match writer.send_buffered().await {
226///     Ok(object) => println!("Successfully created the object {object:?}"),
227///     Err(error) if error.is_serialization() => {
228///         println!("Some problem {error:?} sending the data to the service");
229///         if let Some(m) = error.source().and_then(|e| e.downcast_ref::<WriteError>()) {
230///             println!("{m}");
231///         }
232///     },
233///     Err(e) => return Err(e.into()), // not handled in this example
234/// }
235/// # Ok(()) }
236/// ```
237///
238#[derive(thiserror::Error, Debug)]
239#[non_exhaustive]
240pub enum WriteError {
241    /// The service has "uncommitted" previously persisted bytes.
242    ///
243    /// # Troubleshoot
244    ///
245    /// In the resumable upload protocol the service reports how many bytes are
246    /// persisted. This error indicates that the service previously reported
247    /// more bytes as persisted than in the latest report. This could indicate:
248    /// - a corrupted message from the service, either the earlier message
249    ///   reporting more bytes persisted than actually are, or the current
250    ///   message indicating fewer bytes persisted.
251    /// - a bug in the service, where it reported bytes as persisted when they
252    ///   were not.
253    /// - a bug in the client, maybe storing the incorrect byte count, or
254    ///   parsing the messages incorrectly.
255    ///
256    /// All of these conditions indicate a bug, and in Rust it is idiomatic to
257    /// `panic!()` when a bug is detected. However, in this case it seems more
258    /// appropriate to report the problem, as the client library cannot
259    /// determine the location of the bug.
260    #[error(
261        "the service previously persisted {offset} bytes, but now reports only {persisted} as persisted"
262    )]
263    UnexpectedRewind { offset: u64, persisted: u64 },
264
265    /// The service reports more bytes persisted than sent.
266    ///
267    /// # Troubleshoot
268    ///
269    /// Most likely this indicates that two concurrent uploads are using the
270    /// same session. Review your application design to avoid concurrent
271    /// uploads.
272    ///
273    /// It is possible that this indicates a bug in the service, client, or
274    /// messages corrupted in transit.
275    #[error("the service reports {persisted} bytes as persisted, but we only sent {sent} bytes")]
276    TooMuchProgress { sent: u64, persisted: u64 },
277
278    /// The checksums reported by the service do not match the expected checksums.
279    ///
280    /// # Troubleshoot
281    ///
282    /// The client library compares the CRC32C checksum and/or MD5 hash of the
283    /// uploaded data against the hash reported by the service at the end of
284    /// the upload. This error indicates the hashes did not match.
285    ///
286    /// If you provided known values for these checksums verify those values are
287    /// correct.
288    ///
289    /// Otherwise, this is probably a data corruption problem. These are
290    /// notoriously difficult to root cause. They probably indicate faulty
291    /// equipment, such as the physical machine hosting your client, the network
292    /// elements between your client and the service, or the physical machine
293    /// hosting the service.
294    ///
295    /// If possible, resend the data from a different machine.
296    #[error("checksum mismatch {mismatch} when uploading {} to {}", object.name, object.bucket)]
297    ChecksumMismatch {
298        mismatch: ChecksumMismatch,
299        object: Box<Object>,
300    },
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn mismatch_crc32c() {
309        let value = ChecksumMismatch::Crc32c {
310            got: 0x01020304_u32,
311            want: 0x02030405_u32,
312        };
313        let fmt = value.to_string();
314        assert!(fmt.contains("got=0x01020304"), "{value:?} => {fmt}");
315        assert!(fmt.contains("want=0x02030405"), "{value:?} => {fmt}");
316    }
317
318    #[test]
319    fn mismatch_md5() {
320        let value = ChecksumMismatch::Md5 {
321            got: bytes::Bytes::from_owner([0x01_u8, 0x02, 0x03, 0x04]),
322            want: bytes::Bytes::from_owner([0x02_u8, 0x03, 0x04, 0x05]),
323        };
324        let fmt = value.to_string();
325        assert!(
326            fmt.contains(r#"got=b"\x01\x02\x03\x04""#),
327            "{value:?} => {fmt}"
328        );
329        assert!(
330            fmt.contains(r#"want=b"\x02\x03\x04\x05""#),
331            "{value:?} => {fmt}"
332        );
333    }
334
335    #[test]
336    fn mismatch_both() {
337        let got = ObjectChecksums::new()
338            .set_crc32c(0x01020304_u32)
339            .set_md5_hash(bytes::Bytes::from_owner([0x01_u8, 0x02, 0x03, 0x04]));
340        let want = ObjectChecksums::new()
341            .set_crc32c(0x02030405_u32)
342            .set_md5_hash(bytes::Bytes::from_owner([0x02_u8, 0x03, 0x04, 0x05]));
343        let value = ChecksumMismatch::Both {
344            got: Box::new(got),
345            want: Box::new(want),
346        };
347        let fmt = value.to_string();
348        assert!(fmt.contains("got.crc32c=0x01020304"), "{value:?} => {fmt}");
349        assert!(fmt.contains("want.crc32c=0x02030405"), "{value:?} => {fmt}");
350        assert!(
351            fmt.contains(r#"got.md5=b"\x01\x02\x03\x04""#),
352            "{value:?} => {fmt}"
353        );
354        assert!(
355            fmt.contains(r#"want.md5=b"\x02\x03\x04\x05""#),
356            "{value:?} => {fmt}"
357        );
358    }
359}