diff --git a/api-error-response.go b/api-error-response.go index e0019a334..40b4d7fba 100644 --- a/api-error-response.go +++ b/api-error-response.go @@ -20,7 +20,6 @@ import ( "encoding/xml" "fmt" "net/http" - "strconv" ) /* **** SAMPLE ERROR RESPONSE **** @@ -49,6 +48,9 @@ type ErrorResponse struct { // only in HEAD bucket and ListObjects response. Region string + // Underlying HTTP status code for the returned error + StatusCode int `xml:"-" json:"-"` + // Headers of the returned S3 XML error Headers http.Header `xml:"-" json:"-"` } @@ -100,7 +102,10 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) msg := "Response is empty. " + reportIssue return ErrInvalidArgument(msg) } - var errResp ErrorResponse + + errResp := ErrorResponse{ + StatusCode: resp.StatusCode, + } err := xmlDecoder(resp.Body, &errResp) // Xml decoding failed with no body, fall back to HTTP headers. @@ -109,12 +114,14 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) case http.StatusNotFound: if objectName == "" { errResp = ErrorResponse{ + StatusCode: resp.StatusCode, Code: "NoSuchBucket", Message: "The specified bucket does not exist.", BucketName: bucketName, } } else { errResp = ErrorResponse{ + StatusCode: resp.StatusCode, Code: "NoSuchKey", Message: "The specified key does not exist.", BucketName: bucketName, @@ -123,6 +130,7 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) } case http.StatusForbidden: errResp = ErrorResponse{ + StatusCode: resp.StatusCode, Code: "AccessDenied", Message: "Access Denied.", BucketName: bucketName, @@ -130,12 +138,14 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) } case http.StatusConflict: errResp = ErrorResponse{ + StatusCode: resp.StatusCode, Code: "Conflict", Message: "Bucket not empty.", BucketName: bucketName, } case http.StatusPreconditionFailed: errResp = ErrorResponse{ + StatusCode: resp.StatusCode, Code: "PreconditionFailed", Message: s3ErrorResponseMap["PreconditionFailed"], BucketName: bucketName, @@ -143,6 +153,7 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) } default: errResp = ErrorResponse{ + StatusCode: resp.StatusCode, Code: resp.Status, Message: resp.Status, BucketName: bucketName, @@ -150,7 +161,7 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) } } - // Save hodID, requestID and region information + // Save hostID, requestID and region information // from headers if not available through error XML. if errResp.RequestID == "" { errResp.RequestID = resp.Header.Get("x-amz-request-id") @@ -162,7 +173,7 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) errResp.Region = resp.Header.Get("x-amz-bucket-region") } if errResp.Code == "InvalidRegion" && errResp.Region != "" { - errResp.Message = fmt.Sprintf("Region does not match, expecting region '%s'.", errResp.Region) + errResp.Message = fmt.Sprintf("Region does not match, expecting region ‘%s’.", errResp.Region) } // Save headers returned in the API XML error @@ -173,10 +184,10 @@ func httpRespToErrorResponse(resp *http.Response, bucketName, objectName string) // ErrTransferAccelerationBucket - bucket name is invalid to be used with transfer acceleration. func ErrTransferAccelerationBucket(bucketName string) error { - msg := fmt.Sprintf("The name of the bucket used for Transfer Acceleration must be DNS-compliant and must not contain periods (\".\").") return ErrorResponse{ + StatusCode: http.StatusBadRequest, Code: "InvalidArgument", - Message: msg, + Message: "The name of the bucket used for Transfer Acceleration must be DNS-compliant and must not contain periods ‘.’.", BucketName: bucketName, } } @@ -185,6 +196,7 @@ func ErrTransferAccelerationBucket(bucketName string) error { func ErrEntityTooLarge(totalSize, maxObjectSize int64, bucketName, objectName string) error { msg := fmt.Sprintf("Your proposed upload size ‘%d’ exceeds the maximum allowed object size ‘%d’ for single PUT operation.", totalSize, maxObjectSize) return ErrorResponse{ + StatusCode: http.StatusBadRequest, Code: "EntityTooLarge", Message: msg, BucketName: bucketName, @@ -194,9 +206,10 @@ func ErrEntityTooLarge(totalSize, maxObjectSize int64, bucketName, objectName st // ErrEntityTooSmall - Input size is smaller than supported minimum. func ErrEntityTooSmall(totalSize int64, bucketName, objectName string) error { - msg := fmt.Sprintf("Your proposed upload size ‘%d’ is below the minimum allowed object size '0B' for single PUT operation.", totalSize) + msg := fmt.Sprintf("Your proposed upload size ‘%d’ is below the minimum allowed object size ‘0B’ for single PUT operation.", totalSize) return ErrorResponse{ - Code: "EntityTooLarge", + StatusCode: http.StatusBadRequest, + Code: "EntityTooSmall", Message: msg, BucketName: bucketName, Key: objectName, @@ -205,9 +218,9 @@ func ErrEntityTooSmall(totalSize int64, bucketName, objectName string) error { // ErrUnexpectedEOF - Unexpected end of file reached. func ErrUnexpectedEOF(totalRead, totalSize int64, bucketName, objectName string) error { - msg := fmt.Sprintf("Data read ‘%s’ is not equal to the size ‘%s’ of the input Reader.", - strconv.FormatInt(totalRead, 10), strconv.FormatInt(totalSize, 10)) + msg := fmt.Sprintf("Data read ‘%d’ is not equal to the size ‘%d’ of the input Reader.", totalRead, totalSize) return ErrorResponse{ + StatusCode: http.StatusBadRequest, Code: "UnexpectedEOF", Message: msg, BucketName: bucketName, @@ -218,18 +231,20 @@ func ErrUnexpectedEOF(totalRead, totalSize int64, bucketName, objectName string) // ErrInvalidBucketName - Invalid bucket name response. func ErrInvalidBucketName(message string) error { return ErrorResponse{ - Code: "InvalidBucketName", - Message: message, - RequestID: "minio", + StatusCode: http.StatusBadRequest, + Code: "InvalidBucketName", + Message: message, + RequestID: "minio", } } // ErrInvalidObjectName - Invalid object name response. func ErrInvalidObjectName(message string) error { return ErrorResponse{ - Code: "NoSuchKey", - Message: message, - RequestID: "minio", + StatusCode: http.StatusNotFound, + Code: "NoSuchKey", + Message: message, + RequestID: "minio", } } @@ -240,9 +255,10 @@ var ErrInvalidObjectPrefix = ErrInvalidObjectName // ErrInvalidArgument - Invalid argument response. func ErrInvalidArgument(message string) error { return ErrorResponse{ - Code: "InvalidArgument", - Message: message, - RequestID: "minio", + StatusCode: http.StatusBadRequest, + Code: "InvalidArgument", + Message: message, + RequestID: "minio", } } @@ -250,9 +266,10 @@ func ErrInvalidArgument(message string) error { // The specified bucket does not have a bucket policy. func ErrNoSuchBucketPolicy(message string) error { return ErrorResponse{ - Code: "NoSuchBucketPolicy", - Message: message, - RequestID: "minio", + StatusCode: http.StatusNotFound, + Code: "NoSuchBucketPolicy", + Message: message, + RequestID: "minio", } } @@ -260,8 +277,9 @@ func ErrNoSuchBucketPolicy(message string) error { // The specified API call is not supported func ErrAPINotSupported(message string) error { return ErrorResponse{ - Code: "APINotSupported", - Message: message, - RequestID: "minio", + StatusCode: http.StatusNotImplemented, + Code: "APINotSupported", + Message: message, + RequestID: "minio", } } diff --git a/api-error-response_test.go b/api-error-response_test.go index 595cb50bd..feecdb3cd 100644 --- a/api-error-response_test.go +++ b/api-error-response_test.go @@ -32,20 +32,23 @@ func TestHttpRespToErrorResponse(t *testing.T) { // 'genAPIErrorResponse' generates ErrorResponse for given APIError. // provides a encodable populated response values. genAPIErrorResponse := func(err APIError, bucketName string) ErrorResponse { - var errResp = ErrorResponse{} - errResp.Code = err.Code - errResp.Message = err.Description - errResp.BucketName = bucketName - return errResp + return ErrorResponse{ + Code: err.Code, + Message: err.Description, + BucketName: bucketName, + } } // Encodes the response headers into XML format. - encodeErr := func(response interface{}) []byte { - var bytesBuffer bytes.Buffer - bytesBuffer.WriteString(xml.Header) - encode := xml.NewEncoder(&bytesBuffer) - encode.Encode(response) - return bytesBuffer.Bytes() + encodeErr := func(response ErrorResponse) []byte { + buf := &bytes.Buffer{} + buf.WriteString(xml.Header) + encoder := xml.NewEncoder(buf) + err := encoder.Encode(response) + if err != nil { + t.Fatalf("error encoding response: %v", err) + } + return buf.Bytes() } // `createAPIErrorResponse` Mocks XML error response from the server. @@ -65,6 +68,7 @@ func TestHttpRespToErrorResponse(t *testing.T) { // 'genErrResponse' contructs error response based http Status Code genErrResponse := func(resp *http.Response, code, message, bucketName, objectName string) ErrorResponse { errResp := ErrorResponse{ + StatusCode: resp.StatusCode, Code: code, Message: message, BucketName: bucketName, @@ -80,9 +84,10 @@ func TestHttpRespToErrorResponse(t *testing.T) { // Generate invalid argument error. genInvalidError := func(message string) error { errResp := ErrorResponse{ - Code: "InvalidArgument", - Message: message, - RequestID: "minio", + StatusCode: http.StatusBadRequest, + Code: "InvalidArgument", + Message: message, + RequestID: "minio", } return errResp } @@ -101,22 +106,22 @@ func TestHttpRespToErrorResponse(t *testing.T) { // Set the StatusCode to the argument supplied. // Sets common headers. genEmptyBodyResponse := func(statusCode int) *http.Response { - resp := &http.Response{} - // set empty response body. - resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(""))) - // set headers. + resp := &http.Response{ + StatusCode: statusCode, + Body: ioutil.NopCloser(bytes.NewReader(nil)), + } setCommonHeaders(resp) - // set status code. - resp.StatusCode = statusCode return resp } // Decode XML error message from the http response body. - decodeXMLError := func(resp *http.Response, t *testing.T) error { - var errResp ErrorResponse + decodeXMLError := func(resp *http.Response) error { + errResp := ErrorResponse{ + StatusCode: resp.StatusCode, + } err := xmlDecoder(resp.Body, &errResp) if err != nil { - t.Fatal("XML decoding of response body failed") + t.Fatalf("XML decoding of response body failed: %v", err) } return errResp } @@ -134,12 +139,12 @@ func TestHttpRespToErrorResponse(t *testing.T) { // Used for asserting the actual response. expectedErrResponse := []error{ genInvalidError("Response is empty. " + "Please report this issue at https://github.com/minio/minio-go/issues."), - decodeXMLError(createAPIErrorResponse(APIErrors[0], "minio-bucket"), t), - genErrResponse(setCommonHeaders(&http.Response{}), "NoSuchBucket", "The specified bucket does not exist.", "minio-bucket", ""), - genErrResponse(setCommonHeaders(&http.Response{}), "NoSuchKey", "The specified key does not exist.", "minio-bucket", "Asia/"), - genErrResponse(setCommonHeaders(&http.Response{}), "AccessDenied", "Access Denied.", "minio-bucket", ""), - genErrResponse(setCommonHeaders(&http.Response{}), "Conflict", "Bucket not empty.", "minio-bucket", ""), - genErrResponse(setCommonHeaders(&http.Response{}), "Bad Request", "Bad Request", "minio-bucket", ""), + decodeXMLError(createAPIErrorResponse(APIErrors[0], "minio-bucket")), + genErrResponse(setCommonHeaders(&http.Response{StatusCode: http.StatusNotFound}), "NoSuchBucket", "The specified bucket does not exist.", "minio-bucket", ""), + genErrResponse(setCommonHeaders(&http.Response{StatusCode: http.StatusNotFound}), "NoSuchKey", "The specified key does not exist.", "minio-bucket", "Asia/"), + genErrResponse(setCommonHeaders(&http.Response{StatusCode: http.StatusForbidden}), "AccessDenied", "Access Denied.", "minio-bucket", ""), + genErrResponse(setCommonHeaders(&http.Response{StatusCode: http.StatusConflict}), "Conflict", "Bucket not empty.", "minio-bucket", ""), + genErrResponse(setCommonHeaders(&http.Response{StatusCode: http.StatusBadRequest}), "Bad Request", "Bad Request", "minio-bucket", ""), } // List of http response to be used as input. @@ -182,6 +187,7 @@ func TestHttpRespToErrorResponse(t *testing.T) { func TestErrEntityTooLarge(t *testing.T) { msg := fmt.Sprintf("Your proposed upload size ‘%d’ exceeds the maximum allowed object size ‘%d’ for single PUT operation.", 1000000, 99999) expectedResult := ErrorResponse{ + StatusCode: http.StatusBadRequest, Code: "EntityTooLarge", Message: msg, BucketName: "minio-bucket", @@ -189,22 +195,23 @@ func TestErrEntityTooLarge(t *testing.T) { } actualResult := ErrEntityTooLarge(1000000, 99999, "minio-bucket", "Asia/") if !reflect.DeepEqual(expectedResult, actualResult) { - t.Errorf("Expected result to be '%+v', but instead got '%+v'", expectedResult, actualResult) + t.Errorf("Expected result to be '%#v', but instead got '%#v'", expectedResult, actualResult) } } // Test validates 'ErrEntityTooSmall' error response. func TestErrEntityTooSmall(t *testing.T) { - msg := fmt.Sprintf("Your proposed upload size ‘%d’ is below the minimum allowed object size '0B' for single PUT operation.", -1) + msg := fmt.Sprintf("Your proposed upload size ‘%d’ is below the minimum allowed object size ‘0B’ for single PUT operation.", -1) expectedResult := ErrorResponse{ - Code: "EntityTooLarge", + StatusCode: http.StatusBadRequest, + Code: "EntityTooSmall", Message: msg, BucketName: "minio-bucket", Key: "Asia/", } actualResult := ErrEntityTooSmall(-1, "minio-bucket", "Asia/") if !reflect.DeepEqual(expectedResult, actualResult) { - t.Errorf("Expected result to be '%+v', but instead got '%+v'", expectedResult, actualResult) + t.Errorf("Expected result to be '%#v', but instead got '%#v'", expectedResult, actualResult) } } @@ -213,6 +220,7 @@ func TestErrUnexpectedEOF(t *testing.T) { msg := fmt.Sprintf("Data read ‘%s’ is not equal to the size ‘%s’ of the input Reader.", strconv.FormatInt(100, 10), strconv.FormatInt(101, 10)) expectedResult := ErrorResponse{ + StatusCode: http.StatusBadRequest, Code: "UnexpectedEOF", Message: msg, BucketName: "minio-bucket", @@ -220,46 +228,49 @@ func TestErrUnexpectedEOF(t *testing.T) { } actualResult := ErrUnexpectedEOF(100, 101, "minio-bucket", "Asia/") if !reflect.DeepEqual(expectedResult, actualResult) { - t.Errorf("Expected result to be '%+v', but instead got '%+v'", expectedResult, actualResult) + t.Errorf("Expected result to be '%#v', but instead got '%#v'", expectedResult, actualResult) } } // Test validates 'ErrInvalidBucketName' error response. func TestErrInvalidBucketName(t *testing.T) { expectedResult := ErrorResponse{ - Code: "InvalidBucketName", - Message: "Invalid Bucket name", - RequestID: "minio", + StatusCode: http.StatusBadRequest, + Code: "InvalidBucketName", + Message: "Invalid Bucket name", + RequestID: "minio", } actualResult := ErrInvalidBucketName("Invalid Bucket name") if !reflect.DeepEqual(expectedResult, actualResult) { - t.Errorf("Expected result to be '%+v', but instead got '%+v'", expectedResult, actualResult) + t.Errorf("Expected result to be '%#v', but instead got '%#v'", expectedResult, actualResult) } } // Test validates 'ErrInvalidObjectName' error response. func TestErrInvalidObjectName(t *testing.T) { expectedResult := ErrorResponse{ - Code: "NoSuchKey", - Message: "Invalid Object Key", - RequestID: "minio", + StatusCode: http.StatusNotFound, + Code: "NoSuchKey", + Message: "Invalid Object Key", + RequestID: "minio", } actualResult := ErrInvalidObjectName("Invalid Object Key") if !reflect.DeepEqual(expectedResult, actualResult) { - t.Errorf("Expected result to be '%+v', but instead got '%+v'", expectedResult, actualResult) + t.Errorf("Expected result to be '%#v', but instead got '%#v'", expectedResult, actualResult) } } // Test validates 'ErrInvalidArgument' response. func TestErrInvalidArgument(t *testing.T) { expectedResult := ErrorResponse{ - Code: "InvalidArgument", - Message: "Invalid Argument", - RequestID: "minio", + StatusCode: http.StatusBadRequest, + Code: "InvalidArgument", + Message: "Invalid Argument", + RequestID: "minio", } actualResult := ErrInvalidArgument("Invalid Argument") if !reflect.DeepEqual(expectedResult, actualResult) { - t.Errorf("Expected result to be '%+v', but instead got '%+v'", expectedResult, actualResult) + t.Errorf("Expected result to be '%#v', but instead got '%#v'", expectedResult, actualResult) } }