Files
gofakes3/error.go

351 lines
10 KiB
Go

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 &notQuiteRightResponse{
// 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)
}