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