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