1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
7#[non_exhaustive]
8pub enum S3ErrorCode {
9 #[default]
11 AccessDenied,
13 AccountProblem,
15 BucketAlreadyExists,
17 BucketAlreadyOwnedByYou,
19 BucketNotEmpty,
21 EntityTooLarge,
23 EntityTooSmall,
25 InternalError,
27 InvalidArgument,
29 InvalidBucketName,
31 InvalidBucketState,
33 InvalidDigest,
35 InvalidLocationConstraint,
37 InvalidObjectState,
39 InvalidPart,
41 InvalidPartOrder,
43 InvalidRange,
45 InvalidRequest,
47 InvalidStorageClass,
49 KeyTooLongError,
51 MalformedXML,
53 MetadataTooLarge,
55 MethodNotAllowed,
57 MissingContentLength,
59 NoSuchBucket,
61 NoSuchBucketPolicy,
63 NoSuchCORSConfiguration,
65 NoSuchKey,
67 NoSuchLifecycleConfiguration,
69 NoSuchUpload,
71 NoSuchVersion,
73 NoSuchTagSet,
75 NoSuchWebsiteConfiguration,
77 NotImplemented,
79 ObjectNotInActiveTierError,
81 ObjectLockConfigurationNotFoundError,
83 PreconditionFailed,
85 SignatureDoesNotMatch,
87 TooManyBuckets,
89 XAmzContentSHA256Mismatch,
91 BadDigest,
93 ConditionalRequestConflict,
95 MaxMessageLengthExceeded,
97 NoSuchObjectLockConfiguration,
99 NoSuchPublicAccessBlockConfiguration,
101 NotModified,
103 OwnershipControlsNotFoundError,
105 ReplicationConfigurationNotFoundError,
107 ServerSideEncryptionConfigurationNotFoundError,
109 Custom(&'static str),
111}
112
113impl S3ErrorCode {
114 #[must_use]
116 pub fn as_str(&self) -> &'static str {
117 match self {
118 Self::AccessDenied => "AccessDenied",
119 Self::AccountProblem => "AccountProblem",
120 Self::BucketAlreadyExists => "BucketAlreadyExists",
121 Self::BucketAlreadyOwnedByYou => "BucketAlreadyOwnedByYou",
122 Self::BucketNotEmpty => "BucketNotEmpty",
123 Self::EntityTooLarge => "EntityTooLarge",
124 Self::EntityTooSmall => "EntityTooSmall",
125 Self::InternalError => "InternalError",
126 Self::InvalidArgument => "InvalidArgument",
127 Self::InvalidBucketName => "InvalidBucketName",
128 Self::InvalidBucketState => "InvalidBucketState",
129 Self::InvalidDigest => "InvalidDigest",
130 Self::InvalidLocationConstraint => "InvalidLocationConstraint",
131 Self::InvalidObjectState => "InvalidObjectState",
132 Self::InvalidPart => "InvalidPart",
133 Self::InvalidPartOrder => "InvalidPartOrder",
134 Self::InvalidRange => "InvalidRange",
135 Self::InvalidRequest => "InvalidRequest",
136 Self::InvalidStorageClass => "InvalidStorageClass",
137 Self::KeyTooLongError => "KeyTooLongError",
138 Self::MalformedXML => "MalformedXML",
139 Self::MetadataTooLarge => "MetadataTooLarge",
140 Self::MethodNotAllowed => "MethodNotAllowed",
141 Self::MissingContentLength => "MissingContentLength",
142 Self::NoSuchBucket => "NoSuchBucket",
143 Self::NoSuchBucketPolicy => "NoSuchBucketPolicy",
144 Self::NoSuchCORSConfiguration => "NoSuchCORSConfiguration",
145 Self::NoSuchKey => "NoSuchKey",
146 Self::NoSuchLifecycleConfiguration => "NoSuchLifecycleConfiguration",
147 Self::NoSuchUpload => "NoSuchUpload",
148 Self::NoSuchVersion => "NoSuchVersion",
149 Self::NoSuchTagSet => "NoSuchTagSet",
150 Self::NoSuchWebsiteConfiguration => "NoSuchWebsiteConfiguration",
151 Self::NotImplemented => "NotImplemented",
152 Self::ObjectNotInActiveTierError => "ObjectNotInActiveTierError",
153 Self::ObjectLockConfigurationNotFoundError => "ObjectLockConfigurationNotFoundError",
154 Self::PreconditionFailed => "PreconditionFailed",
155 Self::SignatureDoesNotMatch => "SignatureDoesNotMatch",
156 Self::TooManyBuckets => "TooManyBuckets",
157 Self::XAmzContentSHA256Mismatch => "XAmzContentSHA256Mismatch",
158 Self::BadDigest => "BadDigest",
159 Self::ConditionalRequestConflict => "ConditionalRequestConflict",
160 Self::MaxMessageLengthExceeded => "MaxMessageLengthExceeded",
161 Self::NoSuchObjectLockConfiguration => "NoSuchObjectLockConfiguration",
162 Self::NoSuchPublicAccessBlockConfiguration => "NoSuchPublicAccessBlockConfiguration",
163 Self::NotModified => "NotModified",
164 Self::OwnershipControlsNotFoundError => "OwnershipControlsNotFoundError",
165 Self::ReplicationConfigurationNotFoundError => "ReplicationConfigurationNotFoundError",
166 Self::ServerSideEncryptionConfigurationNotFoundError => {
167 "ServerSideEncryptionConfigurationNotFoundError"
168 }
169 Self::Custom(s) => s,
170 }
171 }
172
173 #[must_use]
175 #[allow(clippy::match_same_arms)]
176 pub fn default_status_code(&self) -> http::StatusCode {
177 match self {
178 Self::NotModified => http::StatusCode::NOT_MODIFIED,
179 Self::BadDigest
180 | Self::EntityTooLarge
181 | Self::EntityTooSmall
182 | Self::InvalidArgument
183 | Self::InvalidBucketName
184 | Self::InvalidDigest
185 | Self::InvalidLocationConstraint
186 | Self::InvalidPart
187 | Self::InvalidPartOrder
188 | Self::InvalidRequest
189 | Self::InvalidStorageClass
190 | Self::KeyTooLongError
191 | Self::MalformedXML
192 | Self::MaxMessageLengthExceeded
193 | Self::MetadataTooLarge
194 | Self::ServerSideEncryptionConfigurationNotFoundError
195 | Self::TooManyBuckets
196 | Self::XAmzContentSHA256Mismatch => http::StatusCode::BAD_REQUEST,
197 Self::AccessDenied
198 | Self::AccountProblem
199 | Self::InvalidObjectState
200 | Self::ObjectNotInActiveTierError
201 | Self::SignatureDoesNotMatch => http::StatusCode::FORBIDDEN,
202 Self::NoSuchBucket
203 | Self::NoSuchBucketPolicy
204 | Self::NoSuchCORSConfiguration
205 | Self::NoSuchKey
206 | Self::NoSuchLifecycleConfiguration
207 | Self::ObjectLockConfigurationNotFoundError
208 | Self::NoSuchObjectLockConfiguration
209 | Self::NoSuchPublicAccessBlockConfiguration
210 | Self::NoSuchUpload
211 | Self::NoSuchVersion
212 | Self::NoSuchTagSet
213 | Self::NoSuchWebsiteConfiguration
214 | Self::OwnershipControlsNotFoundError
215 | Self::ReplicationConfigurationNotFoundError => http::StatusCode::NOT_FOUND,
216 Self::MethodNotAllowed => http::StatusCode::METHOD_NOT_ALLOWED,
217 Self::BucketAlreadyExists
218 | Self::BucketAlreadyOwnedByYou
219 | Self::BucketNotEmpty
220 | Self::ConditionalRequestConflict
221 | Self::InvalidBucketState => http::StatusCode::CONFLICT,
222 Self::MissingContentLength => http::StatusCode::LENGTH_REQUIRED,
223 Self::PreconditionFailed => http::StatusCode::PRECONDITION_FAILED,
224 Self::InvalidRange => http::StatusCode::RANGE_NOT_SATISFIABLE,
225 Self::InternalError => http::StatusCode::INTERNAL_SERVER_ERROR,
226 Self::NotImplemented => http::StatusCode::NOT_IMPLEMENTED,
227 Self::Custom(_) => http::StatusCode::INTERNAL_SERVER_ERROR,
228 }
229 }
230
231 #[must_use]
233 pub fn default_message(&self) -> &'static str {
234 match self {
235 Self::AccessDenied => "Access Denied",
236 Self::AccountProblem => "There is a problem with the account",
237 Self::BucketAlreadyExists => "The requested bucket name is not available",
238 Self::BucketAlreadyOwnedByYou => "The bucket is already owned by you",
239 Self::BucketNotEmpty => "The bucket you tried to delete is not empty",
240 Self::EntityTooLarge => "Your proposed upload exceeds the maximum allowed size",
241 Self::EntityTooSmall => "Your proposed upload is smaller than the minimum allowed size",
242 Self::InternalError => "Internal server error",
243 Self::InvalidArgument => "Invalid Argument",
244 Self::InvalidBucketName => "The specified bucket is not valid",
245 Self::InvalidBucketState => {
246 "The request is not valid with the current state of the bucket"
247 }
248 Self::InvalidDigest => "The Content-MD5 you specified is not valid",
249 Self::InvalidLocationConstraint => "The specified location constraint is not valid",
250 Self::InvalidObjectState => {
251 "The operation is not valid for the current state of the object"
252 }
253 Self::InvalidPart => "One or more of the specified parts could not be found",
254 Self::InvalidPartOrder => "The list of parts was not in ascending order",
255 Self::InvalidRange => "The requested range cannot be satisfied",
256 Self::InvalidRequest => "Invalid Request",
257 Self::InvalidStorageClass => "The storage class you specified is not valid",
258 Self::KeyTooLongError => "Your key is too long",
259 Self::MalformedXML => "The XML you provided was not well-formed",
260 Self::MetadataTooLarge => {
261 "Your metadata headers exceed the maximum allowed metadata size"
262 }
263 Self::MethodNotAllowed => "The specified method is not allowed against this resource",
264 Self::MissingContentLength => "You must provide the Content-Length HTTP header",
265 Self::NoSuchBucket => "The specified bucket does not exist",
266 Self::NoSuchBucketPolicy => "The specified bucket does not have a bucket policy",
267 Self::NoSuchCORSConfiguration => "The CORS configuration does not exist",
268 Self::NoSuchKey => "The specified key does not exist",
269 Self::NoSuchLifecycleConfiguration => "The lifecycle configuration does not exist",
270 Self::NoSuchUpload => "The specified multipart upload does not exist",
271 Self::NoSuchVersion => "The specified version does not exist",
272 Self::NoSuchTagSet => "The TagSet does not exist",
273 Self::NoSuchWebsiteConfiguration => "The website configuration does not exist",
274 Self::NotImplemented => "The functionality is not implemented",
275 Self::ObjectNotInActiveTierError => {
276 "The source object of the COPY operation is not in the active tier"
277 }
278 Self::ObjectLockConfigurationNotFoundError | Self::NoSuchObjectLockConfiguration => {
279 "Object Lock configuration does not exist for this bucket"
280 }
281 Self::PreconditionFailed => {
282 "At least one of the preconditions you specified did not hold"
283 }
284 Self::SignatureDoesNotMatch => "The request signature does not match",
285 Self::TooManyBuckets => "You have attempted to create more buckets than allowed",
286 Self::XAmzContentSHA256Mismatch => {
287 "The provided x-amz-content-sha256 header does not match"
288 }
289 Self::BadDigest => "The Content-MD5 you specified did not match what we received",
290 Self::ConditionalRequestConflict => "The conditional request cannot be processed",
291 Self::MaxMessageLengthExceeded => "Your request was too big",
292 Self::NoSuchPublicAccessBlockConfiguration => {
293 "The public access block configuration was not found"
294 }
295 Self::NotModified => "Not Modified",
296 Self::OwnershipControlsNotFoundError => "The bucket ownership controls were not found",
297 Self::ReplicationConfigurationNotFoundError => {
298 "The replication configuration was not found"
299 }
300 Self::ServerSideEncryptionConfigurationNotFoundError => {
301 "The server-side encryption configuration was not found"
302 }
303 Self::Custom(s) => s,
304 }
305 }
306}
307
308impl fmt::Display for S3ErrorCode {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 f.write_str(self.as_str())
311 }
312}
313
314#[derive(Debug)]
316pub struct S3Error {
317 pub code: S3ErrorCode,
319 pub message: String,
321 pub resource: Option<String>,
323 pub request_id: Option<String>,
325 pub status_code: http::StatusCode,
327 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
329 pub headers: Option<Box<Vec<(String, String)>>>,
333}
334
335impl fmt::Display for S3Error {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 write!(f, "S3Error({}): {}", self.code, self.message)
338 }
339}
340
341impl std::error::Error for S3Error {
342 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
343 self.source
344 .as_ref()
345 .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
346 }
347}
348
349impl S3Error {
350 #[must_use]
352 pub fn new(code: S3ErrorCode) -> Self {
353 let status_code = code.default_status_code();
354 let message = code.default_message().to_owned();
355 Self {
356 code,
357 message,
358 resource: None,
359 request_id: None,
360 status_code,
361 source: None,
362 headers: None,
363 }
364 }
365
366 #[must_use]
368 pub fn with_message(code: S3ErrorCode, message: impl Into<String>) -> Self {
369 Self {
370 status_code: code.default_status_code(),
371 message: message.into(),
372 code,
373 resource: None,
374 request_id: None,
375 source: None,
376 headers: None,
377 }
378 }
379
380 #[must_use]
382 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
383 self.resource = Some(resource.into());
384 self
385 }
386
387 #[must_use]
389 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
390 self.request_id = Some(request_id.into());
391 self
392 }
393
394 #[must_use]
396 pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
397 self.source = Some(Box::new(source));
398 self
399 }
400
401 #[must_use]
403 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
404 self.headers
405 .get_or_insert_with(|| Box::new(Vec::new()))
406 .push((name.into(), value.into()));
407 self
408 }
409
410 #[must_use]
412 pub fn no_such_bucket(bucket_name: impl Into<String>) -> Self {
413 Self::new(S3ErrorCode::NoSuchBucket).with_resource(bucket_name)
414 }
415
416 #[must_use]
418 pub fn no_such_key(key: impl Into<String>) -> Self {
419 Self::new(S3ErrorCode::NoSuchKey).with_resource(key)
420 }
421
422 #[must_use]
424 pub fn no_such_upload(upload_id: impl Into<String>) -> Self {
425 Self::new(S3ErrorCode::NoSuchUpload).with_resource(upload_id)
426 }
427
428 #[must_use]
430 pub fn no_such_version(version_id: impl Into<String>) -> Self {
431 Self::new(S3ErrorCode::NoSuchVersion).with_resource(version_id)
432 }
433
434 #[must_use]
436 pub fn bucket_already_exists(bucket_name: impl Into<String>) -> Self {
437 Self::new(S3ErrorCode::BucketAlreadyExists).with_resource(bucket_name)
438 }
439
440 #[must_use]
442 pub fn bucket_already_owned_by_you(bucket_name: impl Into<String>) -> Self {
443 Self::new(S3ErrorCode::BucketAlreadyOwnedByYou).with_resource(bucket_name)
444 }
445
446 #[must_use]
448 pub fn bucket_not_empty(bucket_name: impl Into<String>) -> Self {
449 Self::new(S3ErrorCode::BucketNotEmpty).with_resource(bucket_name)
450 }
451
452 #[must_use]
454 pub fn access_denied(resource: impl Into<String>) -> Self {
455 Self::new(S3ErrorCode::AccessDenied).with_resource(resource)
456 }
457
458 #[must_use]
460 pub fn internal_error(message: impl Into<String>) -> Self {
461 Self::with_message(S3ErrorCode::InternalError, message)
462 }
463
464 #[must_use]
466 pub fn invalid_argument(message: impl Into<String>) -> Self {
467 Self::with_message(S3ErrorCode::InvalidArgument, message)
468 }
469
470 #[must_use]
472 pub fn invalid_bucket_name(bucket_name: impl Into<String>) -> Self {
473 Self::new(S3ErrorCode::InvalidBucketName).with_resource(bucket_name)
474 }
475
476 #[must_use]
478 pub fn invalid_range(range: impl Into<String>) -> Self {
479 Self::new(S3ErrorCode::InvalidRange).with_resource(range)
480 }
481
482 #[must_use]
484 pub fn invalid_part(part_info: impl Into<String>) -> Self {
485 Self::new(S3ErrorCode::InvalidPart).with_resource(part_info)
486 }
487
488 #[must_use]
490 pub fn invalid_part_order(detail: impl Into<String>) -> Self {
491 Self::new(S3ErrorCode::InvalidPartOrder).with_resource(detail)
492 }
493
494 #[must_use]
496 pub fn malformed_xml(detail: impl Into<String>) -> Self {
497 Self::new(S3ErrorCode::MalformedXML).with_resource(detail)
498 }
499
500 #[must_use]
502 pub fn method_not_allowed(method: impl Into<String>) -> Self {
503 Self::new(S3ErrorCode::MethodNotAllowed).with_resource(method)
504 }
505
506 #[must_use]
508 pub fn not_implemented(detail: impl Into<String>) -> Self {
509 Self::new(S3ErrorCode::NotImplemented).with_resource(detail)
510 }
511
512 #[must_use]
514 pub fn precondition_failed(condition: impl Into<String>) -> Self {
515 Self::new(S3ErrorCode::PreconditionFailed).with_resource(condition)
516 }
517
518 #[must_use]
520 pub fn signature_does_not_match(detail: impl Into<String>) -> Self {
521 Self::new(S3ErrorCode::SignatureDoesNotMatch).with_resource(detail)
522 }
523}
524
525#[macro_export]
540macro_rules! s3_error {
541 ($code:ident) => {
542 $crate::error::S3Error::new($crate::error::S3ErrorCode::$code)
543 };
544 ($code:ident, $msg:expr) => {
545 $crate::error::S3Error::with_message($crate::error::S3ErrorCode::$code, $msg)
546 };
547}