package gofakes3 import ( "fmt" "net/http" "time" xml "github.com/minio/xxml" ) // Error codes are documented here: // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html // // If you add a code to this list, please also add it to ErrorCode.Status(). const ( ErrNone ErrorCode = "" // The Content-MD5 you specified did not match what we received. ErrBadDigest ErrorCode = "BadDigest" ErrBucketAlreadyExists ErrorCode = "BucketAlreadyExists" // Raised when attempting to delete a bucket that still contains items. ErrBucketNotEmpty ErrorCode = "BucketNotEmpty" // "Indicates that the versioning configuration specified in the request is invalid" ErrIllegalVersioningConfiguration ErrorCode = "IllegalVersioningConfigurationException" // You did not provide the number of bytes specified by the Content-Length // HTTP header: ErrIncompleteBody ErrorCode = "IncompleteBody" // POST requires exactly one file upload per request. ErrIncorrectNumberOfFilesInPostRequest ErrorCode = "IncorrectNumberOfFilesInPostRequest" // InlineDataTooLarge occurs when using the PutObjectInline method of the // SOAP interface // (https://docs.aws.amazon.com/AmazonS3/latest/API/SOAPPutObjectInline.html). // This is not documented on the errors page; the error is included here // only for reference. ErrInlineDataTooLarge ErrorCode = "InlineDataTooLarge" ErrInvalidArgument ErrorCode = "InvalidArgument" // https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html#bucketnamingrules ErrInvalidBucketName ErrorCode = "InvalidBucketName" // The Content-MD5 you specified is not valid. ErrInvalidDigest ErrorCode = "InvalidDigest" ErrInvalidRange ErrorCode = "InvalidRange" ErrInvalidToken ErrorCode = "InvalidToken" ErrKeyTooLong ErrorCode = "KeyTooLongError" // This is not a typo: Error is part of the string, but redundant in the constant name ErrMalformedPOSTRequest ErrorCode = "MalformedPOSTRequest" // One or more of the specified parts could not be found. The part might // not have been uploaded, or the specified entity tag might not have // matched the part's entity tag. ErrInvalidPart ErrorCode = "InvalidPart" // The list of parts was not in ascending order. Parts list must be // specified in order by part number. ErrInvalidPartOrder ErrorCode = "InvalidPartOrder" ErrInvalidURI ErrorCode = "InvalidURI" ErrMetadataTooLarge ErrorCode = "MetadataTooLarge" ErrMethodNotAllowed ErrorCode = "MethodNotAllowed" ErrMalformedXML ErrorCode = "MalformedXML" // You must provide the Content-Length HTTP header. ErrMissingContentLength ErrorCode = "MissingContentLength" // See BucketNotFound() for a helper function for this error: ErrNoSuchBucket ErrorCode = "NoSuchBucket" // See KeyNotFound() for a helper function for this error: ErrNoSuchKey ErrorCode = "NoSuchKey" // The specified multipart upload does not exist. The upload ID might be // invalid, or the multipart upload might have been aborted or completed. ErrNoSuchUpload ErrorCode = "NoSuchUpload" ErrNoSuchVersion ErrorCode = "NoSuchVersion" // No need to retransmit the object ErrNotModified ErrorCode = "NotModified" ErrRequestTimeTooSkewed ErrorCode = "RequestTimeTooSkewed" ErrTooManyBuckets ErrorCode = "TooManyBuckets" ErrNotImplemented ErrorCode = "NotImplemented" ErrInternal ErrorCode = "InternalError" ) // INTERNAL errors! These are not part of the S3 interface, they are codes // we have declared ourselves. Should all map to a 500 status code: const ( ErrInternalPageNotImplemented InternalErrorCode = "PaginationNotImplemented" ) // errorResponse should be implemented by any type that needs to be handled by // ensureErrorResponse. type errorResponse interface { Error enrich(requestID string) } func ensureErrorResponse(err error, requestID string) Error { switch err := err.(type) { case errorResponse: err.enrich(requestID) return err case ErrorCode: return &ErrorResponse{ Code: err, RequestID: requestID, Message: string(err), } default: return &ErrorResponse{ Code: ErrInternal, Message: "Internal Error", RequestID: requestID, } } } type Error interface { error ErrorCode() ErrorCode } // ErrorResponse is the base error type returned by S3 when any error occurs. // // Some errors contain their own additional fields in the response, for example // ErrRequestTimeTooSkewed, which contains the server time and the skew limit. // To create one of these responses, subclass it (but please don't export it): // // type notQuiteRightResponse struct { // ErrorResponse // ExtraField int // } // // Next, create a constructor that populates the error. Interfaces won't work // for this job as the error itself does double-duty as the XML response // object. Fill the struct out however you please, but don't forget to assign // Code and Message: // // func NotQuiteRight(at time.Time, max time.Duration) error { // code := ErrNotQuiteRight // return ¬QuiteRightResponse{ // ErrorResponse{Code: code, Message: code.Message()}, // 123456789, // } // } type ErrorResponse struct { XMLName xml.Name `xml:"Error"` Code ErrorCode Message string `xml:",omitempty"` RequestID string `xml:"RequestId,omitempty"` HostID string `xml:"HostId,omitempty"` } func (e *ErrorResponse) ErrorCode() ErrorCode { return e.Code } func (e *ErrorResponse) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } func (r *ErrorResponse) enrich(requestID string) { r.RequestID = requestID } func ErrorMessage(code ErrorCode, message string) error { return &ErrorResponse{Code: code, Message: message} } func ErrorMessagef(code ErrorCode, message string, args ...interface{}) error { return &ErrorResponse{Code: code, Message: fmt.Sprintf(message, args...)} } type ErrorInvalidArgumentResponse struct { ErrorResponse ArgumentName string `xml:"ArgumentName"` ArgumentValue string `xml:"ArgumentValue"` } func ErrorInvalidArgument(name, value, message string) error { return &ErrorInvalidArgumentResponse{ ErrorResponse: ErrorResponse{Code: ErrInvalidArgument, Message: message}, ArgumentName: name, ArgumentValue: value} } // ErrorCode represents an S3 error code, documented here: // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html type ErrorCode string func (e ErrorCode) ErrorCode() ErrorCode { return e } func (e ErrorCode) Error() string { return string(e) } // InternalErrorCode represents an GoFakeS3 error code. It maps to ErrInternal // when constructing a response. type InternalErrorCode string func (e InternalErrorCode) ErrorCode() ErrorCode { return ErrInternal } func (e InternalErrorCode) Error() string { return string(ErrInternal) } // Message tries to return the same string as S3 would return for the error // response, when it is known, or nothing when it is not. If you see the status // text for a code we don't have listed in here in the wild, please let us // know! func (e ErrorCode) Message() string { switch e { case ErrInvalidBucketName: return `Bucket name must match the regex "^[a-zA-Z0-9.\-_]{1,255}$"` case ErrNoSuchBucket: return "The specified bucket does not exist" case ErrRequestTimeTooSkewed: return "The difference between the request time and the current time is too large" case ErrMalformedXML: return "The XML you provided was not well-formed or did not validate against our published schema" default: return "" } } func (e ErrorCode) Status() int { switch e { case ErrBucketAlreadyExists, ErrBucketNotEmpty: return http.StatusConflict case ErrBadDigest, ErrIllegalVersioningConfiguration, ErrIncompleteBody, ErrIncorrectNumberOfFilesInPostRequest, ErrInlineDataTooLarge, ErrInvalidArgument, ErrInvalidBucketName, ErrInvalidDigest, ErrInvalidPart, ErrInvalidPartOrder, ErrInvalidToken, ErrInvalidURI, ErrKeyTooLong, ErrMetadataTooLarge, ErrMethodNotAllowed, ErrMalformedPOSTRequest, ErrMalformedXML, ErrTooManyBuckets: return http.StatusBadRequest case ErrRequestTimeTooSkewed: return http.StatusForbidden case ErrInvalidRange: return http.StatusRequestedRangeNotSatisfiable case ErrNoSuchBucket, ErrNoSuchKey, ErrNoSuchUpload, ErrNoSuchVersion: return http.StatusNotFound case ErrNotImplemented: return http.StatusNotImplemented case ErrNotModified: return http.StatusNotModified case ErrMissingContentLength: return http.StatusLengthRequired case ErrInternal: return http.StatusInternalServerError } return http.StatusInternalServerError } // HasErrorCode asserts that the error has a specific error code: // // if HasErrorCode(err, ErrNoSuchBucket) { // // handle condition // } // // If err is nil and code is ErrNone, HasErrorCode returns true. func HasErrorCode(err error, code ErrorCode) bool { if err == nil && code == "" { return true } s3err, ok := err.(interface{ ErrorCode() ErrorCode }) if !ok { return false } return s3err.ErrorCode() == code } // IsAlreadyExists asserts that the error is a kind that indicates the resource // already exists, similar to os.IsExist. func IsAlreadyExists(err error) bool { return HasErrorCode(err, ErrBucketAlreadyExists) } type resourceErrorResponse struct { ErrorResponse Resource string } var _ errorResponse = &resourceErrorResponse{} func ResourceError(code ErrorCode, resource string) error { return &resourceErrorResponse{ ErrorResponse{Code: code, Message: code.Message()}, resource, } } func BucketNotFound(bucket string) error { return ResourceError(ErrNoSuchBucket, bucket) } func KeyNotFound(key string) error { return ResourceError(ErrNoSuchKey, key) } type requestTimeTooSkewedResponse struct { ErrorResponse ServerTime time.Time MaxAllowedSkewMilliseconds durationAsMilliseconds } var _ errorResponse = &requestTimeTooSkewedResponse{} func requestTimeTooSkewed(at time.Time, max time.Duration) error { code := ErrRequestTimeTooSkewed return &requestTimeTooSkewedResponse{ ErrorResponse{Code: code, Message: code.Message()}, at, durationAsMilliseconds(max), } } // durationAsMilliseconds tricks xml.Marshal into serialising a time.Duration as // truncated milliseconds instead of nanoseconds. type durationAsMilliseconds time.Duration func (m durationAsMilliseconds) MarshalXML(e *xml.Encoder, start xml.StartElement) error { var s = fmt.Sprintf("%d", time.Duration(m)/time.Millisecond) return e.EncodeElement(s, start) }