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::ObjectChecksums;
21
22#[derive(Clone, Debug)]
23#[non_exhaustive]
24pub enum ChecksumMismatch {
25 /// The CRC32C checksum sent by the service does not match the computed (or expected) value.
26 Crc32c { got: u32, want: u32 },
27
28 /// The MD5 hash sent by the service does not match the computed (or expected) value.
29 Md5 {
30 got: bytes::Bytes,
31 want: bytes::Bytes,
32 },
33
34 /// The CRC32C checksum **and** the MD5 hash sent by the service do not
35 /// match the computed (or expected) values.
36 Both {
37 got: Box<ObjectChecksums>,
38 want: Box<ObjectChecksums>,
39 },
40}
41
42impl std::fmt::Display for ChecksumMismatch {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Crc32c { got, want } => write!(
46 f,
47 "the CRC32C checksums do not match: got=0x{got:08x}, want=0x{want:08x}"
48 ),
49 Self::Md5 { got, want } => write!(
50 f,
51 "the MD5 hashes do not match: got={:0x?}, want={:0x?}",
52 &got, &want
53 ),
54 Self::Both { got, want } => {
55 write!(
56 f,
57 "both the CRC32C checksums and MD5 hashes do not match: got.crc32c=0x{:08x}, want.crc32c=0x{:08x}, got.md5={:x?}, want.md5={:x?}",
58 got.crc32c.unwrap_or_default(),
59 want.crc32c.unwrap_or_default(),
60 got.md5_hash,
61 want.md5_hash
62 )
63 }
64 }
65 }
66}
67
68/// Represents errors that can occur when converting to [KeyAes256] instances.
69///
70/// # Example:
71/// ```
72/// # use google_cloud_storage::{builder::storage::KeyAes256, error::KeyAes256Error};
73/// let invalid_key_bytes: &[u8] = b"too_short_key"; // Less than 32 bytes
74/// let result = KeyAes256::new(invalid_key_bytes);
75///
76/// assert!(matches!(result, Err(KeyAes256Error::InvalidLength)));
77/// ```
78///
79/// [KeyAes256]: crate::builder::storage::KeyAes256
80#[derive(thiserror::Error, Debug)]
81#[non_exhaustive]
82pub enum KeyAes256Error {
83 /// The provided key's length was not exactly 32 bytes.
84 #[error("Key has an invalid length: expected 32 bytes.")]
85 InvalidLength,
86}
87
88/// Represents an error that can occur when invalid range is specified.
89#[derive(thiserror::Error, Debug)]
90#[non_exhaustive]
91pub enum RangeError {
92 /// The provided read limit was negative.
93 #[error("read limit was negative, expected non-negative value.")]
94 NegativeLimit,
95 /// A negative offset was provided with a read limit.
96 #[error("negative read offsets cannot be used with read limits.")]
97 NegativeOffsetWithLimit,
98}
99
100/// Represents an error that can occur when reading response data.
101#[derive(thiserror::Error, Debug)]
102#[non_exhaustive]
103pub enum ReadError {
104 /// The calculated crc32c did not match server provided crc32c.
105 #[error("checksum mismatch {0}")]
106 ChecksumMismatch(ChecksumMismatch),
107
108 /// The read was interrupted before all the expected bytes arrived.
109 #[error("missing {0} bytes at the end of the stream")]
110 ShortRead(u64),
111
112 /// The read received more bytes than expected.
113 #[error("too many bytes received: expected {expected}, stopped download at {got}")]
114 LongRead { got: u64, expected: u64 },
115
116 /// Only 200 and 206 status codes are expected in successful responses.
117 #[error("unexpected success code {0} in read request, only 200 and 206 are expected")]
118 UnexpectedSuccessCode(u16),
119
120 /// Successful HTTP response must include some headers.
121 #[error("the response is missing '{0}', a required header")]
122 MissingHeader(&'static str),
123
124 /// The received header format is invalid.
125 #[error("the format for header '{0}' is incorrect")]
126 BadHeaderFormat(
127 &'static str,
128 #[source] Box<dyn std::error::Error + Send + Sync + 'static>,
129 ),
130}
131
132/// An unrecoverable problem in the upload protocol.
133///
134/// # Example
135/// ```
136/// # use google_cloud_storage::{client::Storage, error::UploadError};
137/// # async fn sample(client: &Storage) -> anyhow::Result<()> {
138/// use std::error::Error as _;
139/// let upload = client
140/// .upload_object("projects/_/buckets/my-bucket", "my-object", "hello world")
141/// .with_if_generation_not_match(0);
142/// match upload.send_buffered().await {
143/// Ok(object) => println!("Successfully uploaded the object"),
144/// Err(error) if error.is_serialization() => {
145/// println!("Some problem {error:?} sending the data to the service");
146/// if let Some(m) = error.source().and_then(|e| e.downcast_ref::<UploadError>()) {
147/// println!("{m}");
148/// }
149/// },
150/// Err(e) => return Err(e.into()), // not handled in this example
151/// }
152/// # Ok(()) }
153/// ```
154///
155#[derive(thiserror::Error, Debug)]
156#[non_exhaustive]
157pub enum UploadError {
158 /// The service has "uncommitted" previously persisted bytes.
159 ///
160 /// # Troubleshoot
161 ///
162 /// In the resumable upload protocol the service reports how many bytes are
163 /// persisted. This error indicates that the service previously reported
164 /// more bytes as persisted than in the latest report. This could indicate:
165 /// - a corrupted message from the service, either the earlier message
166 /// reporting more bytes persisted than actually are, or the current
167 /// message indicating fewer bytes persisted.
168 /// - a bug in the service, where it reported bytes as persisted when they
169 /// were not.
170 /// - a bug in the client, maybe storing the incorrect byte count, or
171 /// parsing the messages incorrectly.
172 ///
173 /// All of these conditions indicate a bug, and in Rust it is idiomatic to
174 /// `panic!()` when a bug is detected. However, in this case it seems more
175 /// appropriate to report the problem, as the client library cannot
176 /// determine the location of the bug.
177 #[error(
178 "the service previously persisted {offset} bytes, but now reports only {persisted} as persisted"
179 )]
180 UnexpectedRewind { offset: u64, persisted: u64 },
181
182 /// The service reports more bytes persisted than sent.
183 ///
184 /// # Troubleshoot
185 ///
186 /// Most likely this indicates that two concurrent uploads are using the
187 /// same session. Review your application design to avoid concurrent
188 /// uploads.
189 ///
190 /// It is possible that this indicates a bug in the service, client, or
191 /// messages corrupted in transit.
192 #[error("the service reports {persisted} bytes as persisted, but we only sent {sent} bytes")]
193 TooMuchProgress { sent: u64, persisted: u64 },
194
195 /// The checksums reported by the service do not match the expected checksums.
196 ///
197 /// # Troubleshoot
198 ///
199 /// The client library compares the CRC32C checksum and/or MD5 hash of the
200 /// uploaded data against the hash reported by the service at the end of
201 /// the upload. This error indicates the hashes did not match.
202 ///
203 /// If you provided known values for these checksums verify those values are
204 /// correct.
205 ///
206 /// Otherwise, this is probably a data corruption problem. These are
207 /// notoriously difficult to root cause. They probably indicate faulty
208 /// equipment, such as the physical machine hosting your client, the network
209 /// elements between your client and the service, or the physical machine
210 /// hosting the service.
211 ///
212 /// If possible, resend the data from a different machine.
213 #[error("checksum mismatch {0}")]
214 ChecksumMismatch(ChecksumMismatch),
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn mismatch_crc32c() {
223 let value = ChecksumMismatch::Crc32c {
224 got: 0x01020304_u32,
225 want: 0x02030405_u32,
226 };
227 let fmt = value.to_string();
228 assert!(fmt.contains("got=0x01020304"), "{value:?} => {fmt}");
229 assert!(fmt.contains("want=0x02030405"), "{value:?} => {fmt}");
230 }
231
232 #[test]
233 fn mismatch_md5() {
234 let value = ChecksumMismatch::Md5 {
235 got: bytes::Bytes::from_owner([0x01_u8, 0x02, 0x03, 0x04]),
236 want: bytes::Bytes::from_owner([0x02_u8, 0x03, 0x04, 0x05]),
237 };
238 let fmt = value.to_string();
239 assert!(
240 fmt.contains(r#"got=b"\x01\x02\x03\x04""#),
241 "{value:?} => {fmt}"
242 );
243 assert!(
244 fmt.contains(r#"want=b"\x02\x03\x04\x05""#),
245 "{value:?} => {fmt}"
246 );
247 }
248
249 #[test]
250 fn mismatch_both() {
251 let got = ObjectChecksums::new()
252 .set_crc32c(0x01020304_u32)
253 .set_md5_hash(bytes::Bytes::from_owner([0x01_u8, 0x02, 0x03, 0x04]));
254 let want = ObjectChecksums::new()
255 .set_crc32c(0x02030405_u32)
256 .set_md5_hash(bytes::Bytes::from_owner([0x02_u8, 0x03, 0x04, 0x05]));
257 let value = ChecksumMismatch::Both {
258 got: Box::new(got),
259 want: Box::new(want),
260 };
261 let fmt = value.to_string();
262 assert!(fmt.contains("got.crc32c=0x01020304"), "{value:?} => {fmt}");
263 assert!(fmt.contains("want.crc32c=0x02030405"), "{value:?} => {fmt}");
264 assert!(
265 fmt.contains(r#"got.md5=b"\x01\x02\x03\x04""#),
266 "{value:?} => {fmt}"
267 );
268 assert!(
269 fmt.contains(r#"want.md5=b"\x02\x03\x04\x05""#),
270 "{value:?} => {fmt}"
271 );
272 }
273}