1package gofakes3 2 3import ( 4 "encoding/xml" 5 "fmt" 6 "net/http" 7 "time" 8) 9 10// Error codes are documented here: 11// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html 12// 13// If you add a code to this list, please also add it to ErrorCode.Status(). 14// 15const ( 16 ErrNone ErrorCode = "" 17 18 // The Content-MD5 you specified did not match what we received. 19 ErrBadDigest ErrorCode = "BadDigest" 20 21 ErrBucketAlreadyExists ErrorCode = "BucketAlreadyExists" 22 23 // Raised when attempting to delete a bucket that still contains items. 24 ErrBucketNotEmpty ErrorCode = "BucketNotEmpty" 25 26 // "Indicates that the versioning configuration specified in the request is invalid" 27 ErrIllegalVersioningConfiguration ErrorCode = "IllegalVersioningConfigurationException" 28 29 // You did not provide the number of bytes specified by the Content-Length 30 // HTTP header: 31 ErrIncompleteBody ErrorCode = "IncompleteBody" 32 33 // POST requires exactly one file upload per request. 34 ErrIncorrectNumberOfFilesInPostRequest ErrorCode = "IncorrectNumberOfFilesInPostRequest" 35 36 // InlineDataTooLarge occurs when using the PutObjectInline method of the 37 // SOAP interface 38 // (https://docs.aws.amazon.com/AmazonS3/latest/API/SOAPPutObjectInline.html). 39 // This is not documented on the errors page; the error is included here 40 // only for reference. 41 ErrInlineDataTooLarge ErrorCode = "InlineDataTooLarge" 42 43 ErrInvalidArgument ErrorCode = "InvalidArgument" 44 45 // https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules 46 ErrInvalidBucketName ErrorCode = "InvalidBucketName" 47 48 // The Content-MD5 you specified is not valid. 49 ErrInvalidDigest ErrorCode = "InvalidDigest" 50 51 ErrInvalidRange ErrorCode = "InvalidRange" 52 ErrInvalidToken ErrorCode = "InvalidToken" 53 ErrKeyTooLong ErrorCode = "KeyTooLongError" // This is not a typo: Error is part of the string, but redundant in the constant name 54 ErrMalformedPOSTRequest ErrorCode = "MalformedPOSTRequest" 55 56 // One or more of the specified parts could not be found. The part might 57 // not have been uploaded, or the specified entity tag might not have 58 // matched the part's entity tag. 59 ErrInvalidPart ErrorCode = "InvalidPart" 60 61 // The list of parts was not in ascending order. Parts list must be 62 // specified in order by part number. 63 ErrInvalidPartOrder ErrorCode = "InvalidPartOrder" 64 65 ErrInvalidURI ErrorCode = "InvalidURI" 66 67 ErrMetadataTooLarge ErrorCode = "MetadataTooLarge" 68 ErrMethodNotAllowed ErrorCode = "MethodNotAllowed" 69 ErrMalformedXML ErrorCode = "MalformedXML" 70 71 // You must provide the Content-Length HTTP header. 72 ErrMissingContentLength ErrorCode = "MissingContentLength" 73 74 // See BucketNotFound() for a helper function for this error: 75 ErrNoSuchBucket ErrorCode = "NoSuchBucket" 76 77 // See KeyNotFound() for a helper function for this error: 78 ErrNoSuchKey ErrorCode = "NoSuchKey" 79 80 // The specified multipart upload does not exist. The upload ID might be 81 // invalid, or the multipart upload might have been aborted or completed. 82 ErrNoSuchUpload ErrorCode = "NoSuchUpload" 83 84 ErrNoSuchVersion ErrorCode = "NoSuchVersion" 85 86 ErrRequestTimeTooSkewed ErrorCode = "RequestTimeTooSkewed" 87 ErrTooManyBuckets ErrorCode = "TooManyBuckets" 88 ErrNotImplemented ErrorCode = "NotImplemented" 89 90 ErrInternal ErrorCode = "InternalError" 91) 92 93// INTERNAL errors! These are not part of the S3 interface, they are codes 94// we have declared ourselves. Should all map to a 500 status code: 95const ( 96 ErrInternalPageNotImplemented InternalErrorCode = "PaginationNotImplemented" 97) 98 99// errorResponse should be implemented by any type that needs to be handled by 100// ensureErrorResponse. 101type errorResponse interface { 102 Error 103 enrich(requestID string) 104} 105 106func ensureErrorResponse(err error, requestID string) Error { 107 switch err := err.(type) { 108 case errorResponse: 109 err.enrich(requestID) 110 return err 111 112 case ErrorCode: 113 return &ErrorResponse{ 114 Code: err, 115 RequestID: requestID, 116 Message: string(err), 117 } 118 119 default: 120 return &ErrorResponse{ 121 Code: ErrInternal, 122 Message: "Internal Error", 123 RequestID: requestID, 124 } 125 } 126} 127 128type Error interface { 129 error 130 ErrorCode() ErrorCode 131} 132 133// ErrorResponse is the base error type returned by S3 when any error occurs. 134// 135// Some errors contain their own additional fields in the response, for example 136// ErrRequestTimeTooSkewed, which contains the server time and the skew limit. 137// To create one of these responses, subclass it (but please don't export it): 138// 139// type notQuiteRightResponse struct { 140// ErrorResponse 141// ExtraField int 142// } 143// 144// Next, create a constructor that populates the error. Interfaces won't work 145// for this job as the error itself does double-duty as the XML response 146// object. Fill the struct out however you please, but don't forget to assign 147// Code and Message: 148// 149// func NotQuiteRight(at time.Time, max time.Duration) error { 150// code := ErrNotQuiteRight 151// return ¬QuiteRightResponse{ 152// ErrorResponse{Code: code, Message: code.Message()}, 153// 123456789, 154// } 155// } 156// 157type ErrorResponse struct { 158 XMLName xml.Name `xml:"Error"` 159 160 Code ErrorCode 161 Message string `xml:",omitempty"` 162 RequestID string `xml:"RequestId,omitempty"` 163 HostID string `xml:"HostId,omitempty"` 164} 165 166func (e *ErrorResponse) ErrorCode() ErrorCode { return e.Code } 167 168func (e *ErrorResponse) Error() string { 169 return fmt.Sprintf("%s: %s", e.Code, e.Message) 170} 171 172func (r *ErrorResponse) enrich(requestID string) { 173 r.RequestID = requestID 174} 175 176func ErrorMessage(code ErrorCode, message string) error { 177 return &ErrorResponse{Code: code, Message: message} 178} 179 180func ErrorMessagef(code ErrorCode, message string, args ...interface{}) error { 181 return &ErrorResponse{Code: code, Message: fmt.Sprintf(message, args...)} 182} 183 184type ErrorInvalidArgumentResponse struct { 185 ErrorResponse 186 187 ArgumentName string `xml:"ArgumentName"` 188 ArgumentValue string `xml:"ArgumentValue"` 189} 190 191func ErrorInvalidArgument(name, value, message string) error { 192 return &ErrorInvalidArgumentResponse{ 193 ErrorResponse: ErrorResponse{Code: ErrInvalidArgument, Message: message}, 194 ArgumentName: name, ArgumentValue: value} 195} 196 197// ErrorCode represents an S3 error code, documented here: 198// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html 199type ErrorCode string 200 201func (e ErrorCode) ErrorCode() ErrorCode { return e } 202func (e ErrorCode) Error() string { return string(e) } 203 204// InternalErrorCode represents an GoFakeS3 error code. It maps to ErrInternal 205// when constructing a response. 206type InternalErrorCode string 207 208func (e InternalErrorCode) ErrorCode() ErrorCode { return ErrInternal } 209func (e InternalErrorCode) Error() string { return string(ErrInternal) } 210 211// Message tries to return the same string as S3 would return for the error 212// response, when it is known, or nothing when it is not. If you see the status 213// text for a code we don't have listed in here in the wild, please let us 214// know! 215func (e ErrorCode) Message() string { 216 switch e { 217 case ErrInvalidBucketName: 218 return `Bucket name must match the regex "^[a-zA-Z0-9.\-_]{1,255}$"` 219 case ErrNoSuchBucket: 220 return "The specified bucket does not exist" 221 case ErrRequestTimeTooSkewed: 222 return "The difference between the request time and the current time is too large" 223 case ErrMalformedXML: 224 return "The XML you provided was not well-formed or did not validate against our published schema" 225 default: 226 return "" 227 } 228} 229 230func (e ErrorCode) Status() int { 231 switch e { 232 case ErrBucketAlreadyExists, 233 ErrBucketNotEmpty: 234 return http.StatusConflict 235 236 case ErrBadDigest, 237 ErrIllegalVersioningConfiguration, 238 ErrIncompleteBody, 239 ErrIncorrectNumberOfFilesInPostRequest, 240 ErrInlineDataTooLarge, 241 ErrInvalidArgument, 242 ErrInvalidBucketName, 243 ErrInvalidDigest, 244 ErrInvalidPart, 245 ErrInvalidPartOrder, 246 ErrInvalidToken, 247 ErrInvalidURI, 248 ErrKeyTooLong, 249 ErrMetadataTooLarge, 250 ErrMethodNotAllowed, 251 ErrMalformedPOSTRequest, 252 ErrMalformedXML, 253 ErrTooManyBuckets: 254 return http.StatusBadRequest 255 256 case ErrRequestTimeTooSkewed: 257 return http.StatusForbidden 258 259 case ErrInvalidRange: 260 return http.StatusRequestedRangeNotSatisfiable 261 262 case ErrNoSuchBucket, 263 ErrNoSuchKey, 264 ErrNoSuchUpload, 265 ErrNoSuchVersion: 266 return http.StatusNotFound 267 268 case ErrNotImplemented: 269 return http.StatusNotImplemented 270 271 case ErrMissingContentLength: 272 return http.StatusLengthRequired 273 274 case ErrInternal: 275 return http.StatusInternalServerError 276 } 277 278 return http.StatusInternalServerError 279} 280 281// HasErrorCode asserts that the error has a specific error code: 282// 283// if HasErrorCode(err, ErrNoSuchBucket) { 284// // handle condition 285// } 286// 287// If err is nil and code is ErrNone, HasErrorCode returns true. 288// 289func HasErrorCode(err error, code ErrorCode) bool { 290 if err == nil && code == "" { 291 return true 292 } 293 s3err, ok := err.(interface{ ErrorCode() ErrorCode }) 294 if !ok { 295 return false 296 } 297 return s3err.ErrorCode() == code 298} 299 300// IsAlreadyExists asserts that the error is a kind that indicates the resource 301// already exists, similar to os.IsExist. 302func IsAlreadyExists(err error) bool { 303 return HasErrorCode(err, ErrBucketAlreadyExists) 304} 305 306type resourceErrorResponse struct { 307 ErrorResponse 308 Resource string 309} 310 311var _ errorResponse = &resourceErrorResponse{} 312 313func ResourceError(code ErrorCode, resource string) error { 314 return &resourceErrorResponse{ 315 ErrorResponse{Code: code, Message: code.Message()}, 316 resource, 317 } 318} 319 320func BucketNotFound(bucket string) error { return ResourceError(ErrNoSuchBucket, bucket) } 321func KeyNotFound(key string) error { return ResourceError(ErrNoSuchKey, key) } 322 323type requestTimeTooSkewedResponse struct { 324 ErrorResponse 325 ServerTime time.Time 326 MaxAllowedSkewMilliseconds durationAsMilliseconds 327} 328 329var _ errorResponse = &requestTimeTooSkewedResponse{} 330 331func requestTimeTooSkewed(at time.Time, max time.Duration) error { 332 code := ErrRequestTimeTooSkewed 333 return &requestTimeTooSkewedResponse{ 334 ErrorResponse{Code: code, Message: code.Message()}, 335 at, durationAsMilliseconds(max), 336 } 337} 338 339// durationAsMilliseconds tricks xml.Marsha into serialising a time.Duration as 340// truncated milliseconds instead of nanoseconds. 341type durationAsMilliseconds time.Duration 342 343func (m durationAsMilliseconds) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 344 var s = fmt.Sprintf("%d", time.Duration(m)/time.Millisecond) 345 return e.EncodeElement(s, start) 346} 347