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