feat: global error refactoring

Fixes #2123
Fixes #2120
Fixes #2116
Fixes #2111
Fixes #2108
Fixes #2086
Fixes #2085
Fixes #2083
Fixes #2081
Fixes #2080
Fixes #2073
Fixes #2072
Fixes #2071
Fixes #2069
Fixes #2044
Fixes #2043
Fixes #2042
Fixes #2041
Fixes #2040
Fixes #2039
Fixes #2036
Fixes #2035
Fixes #2034
Fixes #2028
Fixes #2020
Fixes #1842
Fixes #1810
Fixes #1780
Fixes #1775
Fixes #1736
Fixes #1705
Fixes #1663
Fixes #1645
Fixes #1583
Fixes #1526
Fixes #1514
Fixes #1493
Fixes #1487
Fixes #959
Fixes #779
Closes #823
Closes #85

Refactor global S3 error handling around structured error types and centralized XML response generation.

All S3 errors now share the common APIError base for the fields every error has: Code, HTTP status code, and Message. Non-traditional errors that need AWS-compatible XML fields now have dedicated typed errors in the s3err package. Each typed error implements the shared S3Error behavior so controllers and middleware can handle errors consistently while still emitting error-specific XML fields.

Add a dedicated InvalidArgumentError type because InvalidArgument is used widely across request validation, auth, copy source handling, object lock validation, multipart validation, and header parsing. The new InvalidArgument path uses explicit InvalidArgErrorCode constants with predefined descriptions and ArgumentName values, keeping call sites readable while preserving the correct InvalidArgument XML shape and optional ArgumentValue.

New structured errors added in s3err:
- `AccessForbiddenError`: Method, ResourceType
- `BadDigestError`: CalculatedDigest, ExpectedDigest
- `BucketError`: BucketName
- `ContentSHA256MismatchError`: ClientComputedContentSHA256, S3ComputedContentSHA256
- `EntityTooLargeError`: ProposedSize, MaxSizeAllowed
- `EntityTooSmallError`: ProposedSize, MinSizeAllowed
- `ExpiredPresignedURLError`: ServerTime, XAmzExpires, Expires
- `InvalidAccessKeyIdError`: AWSAccessKeyId
- `InvalidArgumentError`: Description, ArgumentName, ArgumentValue
- `InvalidChunkSizeError`: Chunk, BadChunkSize
- `InvalidDigestError`: ContentMD5
- `InvalidLocationConstraintError`: LocationConstraint
- `InvalidPartError`: UploadId, PartNumber, ETag
- `InvalidRangeError`: RangeRequested, ActualObjectSize
- `InvalidTagError`: TagKey, TagValue
- `KeyTooLongError`: Size, MaxSizeAllowed
- `MetadataTooLargeError`: Size, MaxSizeAllowed
- `MethodNotAllowedError`: Method, ResourceType, AllowedMethods
- `NoSuchUploadError`: UploadId
- `NoSuchVersionError`: Key, VersionId
- `NotImplementedError`: Header, AdditionalMessage
- `PreconditionFailedError`: Condition
- `RequestTimeTooSkewedError`: RequestTime, ServerTime, MaxAllowedSkewMilliseconds
- `SignatureDoesNotMatchError`: AWSAccessKeyId, StringToSign, SignatureProvided, StringToSignBytes, CanonicalRequest, CanonicalRequestBytes

Fix CompleteMultipartUpload validation in the Azure backend so missing or empty `ETag` values return the appropriate S3 error instead of allowing a gateway panic.

Fix presigned authentication expiration validation to compare server time in `UTC`, matching the `UTC` timestamp used by presigned URL signing.

Add request ID and host ID support across S3 requests. Each request now receives AWS S3-like identifiers, returned in response headers as `x-amz-request-id` and `x-amz-id-2` and included in all XML error responses as RequestId and HostId. The generated ID structure is designed to resemble AWS S3 request IDs and host IDs.

The request signature calculation/validation for streaming uploads was previously delayed until the request body was fully read, both for Authorization header authentication and presigned URLs.
Now, the signature is validated immediately in the authorization middlewares without reading the request body, since the signature calculation itself does not depend on the request body. Instead, only the `x-amz-content-sha256` SHA-256 hash calculation is delayed.
This commit is contained in:
niksis02
2026-05-21 23:49:34 +04:00
parent eade1e3a71
commit 9f786b3c2c
132 changed files with 3511 additions and 1339 deletions
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// AccessForbiddenError is returned when a CORS request is not allowed.
// Produces <Method> and <ResourceType> fields in the XML response.
type AccessForbiddenError struct {
APIError
Method string
ResourceType ResourceType
}
func (e AccessForbiddenError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Method string `xml:",omitempty"`
ResourceType ResourceType `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Method: e.Method,
ResourceType: e.ResourceType,
RequestID: requestID,
HostID: hostID,
})
}
func (e AccessForbiddenError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetAccessForbiddenErr(code ErrorCode, method string, resourceType ResourceType) AccessForbiddenError {
return AccessForbiddenError{
APIError: GetAPIError(code),
Method: method,
ResourceType: resourceType,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// BadDigestError is returned when the Content-MD5 does not match the received data.
// Produces <CalculatedDigest> and <ExpectedDigest> fields in the XML response.
type BadDigestError struct {
APIError
CalculatedDigest string
ExpectedDigest string
}
func (e BadDigestError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
CalculatedDigest string `xml:",omitempty"`
ExpectedDigest string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
CalculatedDigest: e.CalculatedDigest,
ExpectedDigest: e.ExpectedDigest,
RequestID: requestID,
HostID: hostID,
})
}
func (e BadDigestError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetBadDigestErr(calculated, expected string) BadDigestError {
return BadDigestError{
APIError: GetAPIError(ErrBadDigest),
CalculatedDigest: calculated,
ExpectedDigest: expected,
}
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// BucketError is returned for errors that include the bucket name.
// Produces a <BucketName> field in the XML response.
type BucketError struct {
APIError
BucketName string
}
func (e BucketError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
BucketName string
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
BucketName: e.BucketName,
RequestID: requestID,
HostID: hostID,
})
}
func (e BucketError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
// GetBucketErr creates a BucketError for the given error code and bucket name.
// Use for: ErrNoSuchBucketPolicy, ErrOwnershipControlsNotFound, ErrBucketNotEmpty,
// ErrNoSuchCORSConfiguration, and similar errors that should include the bucket name.
func GetBucketErr(code ErrorCode, bucket string) BucketError {
return BucketError{
APIError: GetAPIError(code),
BucketName: bucket,
}
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// ContentSHA256MismatchError is returned when the x-amz-content-sha256 header does not match
// the computed hash of the request payload.
// Produces <ClientComputedContentSHA256> and <S3ComputedContentSHA256> fields.
type ContentSHA256MismatchError struct {
APIError
ClientComputedContentSHA256 string
S3ComputedContentSHA256 string
}
func (e ContentSHA256MismatchError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ClientComputedContentSHA256 string `xml:",omitempty"`
S3ComputedContentSHA256 string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
ClientComputedContentSHA256: e.ClientComputedContentSHA256,
S3ComputedContentSHA256: e.S3ComputedContentSHA256,
RequestID: requestID,
HostID: hostID,
})
}
func (e ContentSHA256MismatchError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetContentSHA256MismatchErr(clientHash, s3Hash string) ContentSHA256MismatchError {
return ContentSHA256MismatchError{
APIError: GetAPIError(ErrContentSHA256Mismatch),
ClientComputedContentSHA256: clientHash,
S3ComputedContentSHA256: s3Hash,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// EntityTooLargeError is returned when the proposed upload exceeds the maximum allowed size.
// Produces <ProposedSize> and <MaxSizeAllowed> fields in the XML response.
type EntityTooLargeError struct {
APIError
ProposedSize int64
MaxSizeAllowed int64
}
func (e EntityTooLargeError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ProposedSize int64 `xml:",omitempty"`
MaxSizeAllowed int64 `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
ProposedSize: e.ProposedSize,
MaxSizeAllowed: e.MaxSizeAllowed,
RequestID: requestID,
HostID: hostID,
})
}
func (e EntityTooLargeError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetEntityTooLargeErr(proposedSize, maxSizeAllowed int64) EntityTooLargeError {
return EntityTooLargeError{
APIError: GetAPIError(ErrEntityTooLarge),
ProposedSize: proposedSize,
MaxSizeAllowed: maxSizeAllowed,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// EntityTooSmallError is returned when the proposed upload is smaller than the minimum allowed size.
// Produces <ProposedSize> and <MinSizeAllowed> fields in the XML response.
type EntityTooSmallError struct {
APIError
ProposedSize int64
MinSizeAllowed int64
}
func (e EntityTooSmallError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ProposedSize int64 `xml:",omitempty"`
MinSizeAllowed int64 `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
ProposedSize: e.ProposedSize,
MinSizeAllowed: e.MinSizeAllowed,
RequestID: requestID,
HostID: hostID,
})
}
func (e EntityTooSmallError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetEntityTooSmallErr(proposedSize, minSizeAllowed int64) EntityTooSmallError {
return EntityTooSmallError{
APIError: GetAPIError(ErrEntityTooSmall),
ProposedSize: proposedSize,
MinSizeAllowed: minSizeAllowed,
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// ExpiredPresignedURLError is returned when the presigned url is expired
// Produces <ServerTime>, <X-Amz-Expires> and <Expires> fields in the XML response.
type ExpiredPresignedURLError struct {
APIError
ServerTime string
XAmzExpires int
Expires string
}
func (e ExpiredPresignedURLError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ServerTime string `xml:",omitempty"`
XAmzExpires int `xml:"X-Amz-Expires,omitempty"`
Expires string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
ServerTime: e.ServerTime,
XAmzExpires: e.XAmzExpires,
Expires: e.Expires,
RequestID: requestID,
HostID: hostID,
})
}
func (e ExpiredPresignedURLError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetExpiredPresignedURLError(XAmzExpires int, expires, serverTime string) ExpiredPresignedURLError {
return ExpiredPresignedURLError{
APIError: GetAPIError(ErrExpiredPresignRequest),
XAmzExpires: XAmzExpires,
Expires: expires,
ServerTime: serverTime,
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidAccessKeyIdError is returned when the provided AWS access key ID does not exist.
// Produces an <AWSAccessKeyId> field in the XML response.
type InvalidAccessKeyIdError struct {
APIError
AWSAccessKeyId string
}
func (e InvalidAccessKeyIdError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
AWSAccessKeyId string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
AWSAccessKeyId: e.AWSAccessKeyId,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidAccessKeyIdError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidAccessKeyIdErr(accessKeyId string) InvalidAccessKeyIdError {
return InvalidAccessKeyIdError{
APIError: GetAPIError(ErrInvalidAccessKeyID),
AWSAccessKeyId: accessKeyId,
}
}
+274
View File
@@ -0,0 +1,274 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import (
"bytes"
"encoding/xml"
"fmt"
"net/http"
)
type InvalidArgErrorCode int
const (
InvalidArgMaxBuckets InvalidArgErrorCode = iota
InvalidArgNegativeMaxKeys
InvalidArgObjectAttributes
InvalidArgPartNumber
InvalidArgCompleteMpPartNumber
InvalidArgCopySourceRange
InvalidArgCopySourceBucket
InvalidArgCopySourceObject
InvalidArgCopySourceEncoding
InvalidArgURLEncodedTagging
InvalidArgAuthHeader
InvalidArgAuthorizationType
InvalidArgPOSTFileRequired
InvalidArgSHA256Payload
InvalidArgCopySource
InvalidArgRetainUntilDate
InvalidArgPastObjectLockRetainDate
InvalidArgObjectLockRetentionDays
InvalidArgObjectLockRetentionYears
InvalidArgMissingObjectLockRetainDate
InvalidArgMissingObjectLockMode
InvalidArgLegalHoldStatus
InvalidArgObjectLockMode
InvalidArgMetadataDirective
InvalidArgTaggingDirective
InvalidArgVersionId
InvalidArgChecksumPart
InvalidArgMissingUploadId
InvalidArgUploadIdMarker
InvalidArgCannedAcl
InvalidArgOnlyAws4HmacSha256
InvalidArgDateHeader
)
var invalidArgErrResponses = map[InvalidArgErrorCode]InvalidArgumentError{
InvalidArgMaxBuckets: {
Description: "Argument max-buckets must be an integer between 1 and 10000.",
ArgumentName: "max-buckets",
},
InvalidArgNegativeMaxKeys: {
Description: "max-keys cannot be negative",
ArgumentName: "maxKeys",
},
InvalidArgObjectAttributes: {
Description: "Invalid attribute name specified.",
ArgumentName: "x-amz-object-attributes",
},
InvalidArgPartNumber: {
Description: "Part number must be an integer between 1 and 10000, inclusive.",
ArgumentName: "partNumber",
},
InvalidArgCompleteMpPartNumber: {
Description: "PartNumber must be >= 1",
ArgumentName: "PartNumber",
},
InvalidArgCopySourceRange: {
Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
ArgumentName: "x-amz-copy-source-range",
},
InvalidArgCopySourceBucket: {
Description: "Invalid copy source bucket name",
ArgumentName: "x-amz-copy-source",
},
InvalidArgCopySourceObject: {
Description: "Invalid copy source object key",
ArgumentName: "x-amz-copy-source",
},
InvalidArgCopySourceEncoding: {
Description: "Invalid copy source encoding",
ArgumentName: "x-amz-copy-source",
},
InvalidArgURLEncodedTagging: {
Description: "The header 'x-amz-tagging' shall be encoded as UTF-8 then URLEncoded URL query parameters without tag name duplicates.",
ArgumentName: "x-amz-tagging",
},
InvalidArgAuthHeader: {
Description: "Authorization header is invalid -- one and only one ' ' (space) required.",
ArgumentName: "Authorization",
},
InvalidArgAuthorizationType: {
Description: "Unsupported Authorization Type",
ArgumentName: "Authorization",
},
InvalidArgPOSTFileRequired: {
Description: "POST requires exactly one file upload per request.",
ArgumentName: "file",
},
InvalidArgSHA256Payload: {
Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.",
ArgumentName: "x-amz-content-sha256",
},
InvalidArgCopySource: {
Description: "You can only specify a copy source header for copy requests.",
ArgumentName: "x-amz-copy-source",
},
InvalidArgRetainUntilDate: {
Description: "The retain until date must be provided in ISO 8601 format",
ArgumentName: "x-amz-object-lock-retain-until-date",
},
InvalidArgPastObjectLockRetainDate: {
Description: "The retain until date must be in the future!",
ArgumentName: "x-amz-object-lock-retain-until-date",
},
InvalidArgMissingObjectLockRetainDate: {
Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied.",
ArgumentName: "x-amz-object-lock-retain-until-date",
},
InvalidArgMissingObjectLockMode: {
Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied.",
ArgumentName: "x-amz-object-lock-mode",
},
InvalidArgObjectLockRetentionDays: {
Description: "Default retention period must be a positive integer value.",
ArgumentName: "Days",
},
InvalidArgObjectLockRetentionYears: {
Description: "Default retention period must be a positive integer value.",
ArgumentName: "Years",
},
InvalidArgLegalHoldStatus: {
Description: "Legal Hold must be either of 'ON' or 'OFF'",
ArgumentName: "x-amz-object-lock-legal-hold",
},
InvalidArgObjectLockMode: {
Description: "Unknown wormMode directive.",
ArgumentName: "x-amz-object-lock-mode",
},
InvalidArgMetadataDirective: {
Description: "Unknown metadata directive.",
ArgumentName: "x-amz-metadata-directive",
},
InvalidArgTaggingDirective: {
Description: "Unknown tagging directive.",
ArgumentName: "x-amz-tagging-directive",
},
InvalidArgVersionId: {
Description: "Invalid version id specified",
ArgumentName: "versionId",
},
InvalidArgChecksumPart: {
Description: "Invalid Base64 or multiple checksums present in request",
ArgumentName: "Checksum",
},
InvalidArgMissingUploadId: {
Description: "This operation does not accept partNumber without uploadId",
ArgumentName: "partNumber",
},
InvalidArgUploadIdMarker: {
Description: "Invalid uploadId marker",
ArgumentName: "upload-id-marker",
},
InvalidArgCannedAcl: {
Description: "",
ArgumentName: "x-amz-acl",
},
InvalidArgOnlyAws4HmacSha256: {
Description: "Only AWS4-HMAC-SHA256 is supported",
ArgumentName: "X-Amz-Algorithm",
},
InvalidArgDateHeader: {
Description: "X-Amz-Date must be formated via ISO8601 Long format",
ArgumentName: "X-Amz-Date",
},
}
// InvalidArgumentError is returned when a request argument is invalid.
// Produces <ArgumentName> and <ArgumentValue> fields in the XML response.
type InvalidArgumentError struct {
Description string
ArgumentName string
ArgumentValue string
}
func (e InvalidArgumentError) BaseError() APIError {
return APIError{
Code: "InvalidArgument",
Description: e.Description,
HTTPStatusCode: http.StatusBadRequest,
}
}
// InvalidArgumentError http status code is always 400
func (e InvalidArgumentError) StatusCode() int { return http.StatusBadRequest }
func (e InvalidArgumentError) Error() string {
var bytesBuffer bytes.Buffer
bytesBuffer.WriteString(xml.Header)
enc := xml.NewEncoder(&bytesBuffer)
_ = enc.Encode(e)
return bytesBuffer.String()
}
func (e InvalidArgumentError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ArgumentName string `xml:"ArgumentName,omitempty"`
ArgumentValue string `xml:"ArgumentValue,omitempty"`
RequestId string `xml:"RequestId,omitempty"`
HostId string `xml:"HostId,omitempty"`
}{
Code: "InvalidArgument",
Message: e.Description,
ArgumentName: e.ArgumentName,
ArgumentValue: e.ArgumentValue,
RequestId: requestID,
HostId: hostID,
})
}
func GetInvalidArgumentErr(code InvalidArgErrorCode, value string) InvalidArgumentError {
err := invalidArgErrResponses[code]
err.ArgumentValue = value
return err
}
func GetInvalidArgMaxLimiter(name, value string) InvalidArgumentError {
return InvalidArgumentError{
ArgumentName: name,
ArgumentValue: value,
Description: fmt.Sprintf("Provided %s not an integer or within integer range", value),
}
}
func GetInvalidArgNegativeMaxLimiter(name, value string) InvalidArgumentError {
return InvalidArgumentError{
ArgumentName: name,
ArgumentValue: value,
Description: fmt.Sprintf("Argument %s must be an integer between 0 and 2147483647", value),
}
}
func GetInvalidArgExceedingRange(size int64) InvalidArgumentError {
return InvalidArgumentError{
ArgumentName: "x-amz-copy-source-range",
ArgumentValue: fmt.Sprint(size),
Description: fmt.Sprintf("Range specified is not valid for source object of size: %d", size),
}
}
func GetInvalidArgObjectOwnership(value string) InvalidArgumentError {
return InvalidArgumentError{
ArgumentName: "x-amz-object-ownership",
// no ArgumentValue is returned for this error
Description: fmt.Sprintf("Invalid x-amz-object-ownership header: %s", value),
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidChunkSizeError is returned when a chunk size in a streaming upload is invalid.
// Produces <Chunk> and <BadChunkSize> fields in the XML response.
type InvalidChunkSizeError struct {
APIError
Chunk int
BadChunkSize int64
}
func (e InvalidChunkSizeError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Chunk int `xml:",omitempty"`
BadChunkSize int64 `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Chunk: e.Chunk,
BadChunkSize: e.BadChunkSize,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidChunkSizeError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidChunkSizeErr(chunk int, badChunkSize int64) InvalidChunkSizeError {
return InvalidChunkSizeError{
APIError: GetAPIError(ErrInvalidChunkSize),
Chunk: chunk,
BadChunkSize: badChunkSize,
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidDigestError is returned when the Content-MD5 header value is invalid.
// Produces a <Content-MD5> field in the XML response.
type InvalidDigestError struct {
APIError
ContentMD5 string
}
func (e InvalidDigestError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ContentMD5 string `xml:"Content-MD5"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
ContentMD5: e.ContentMD5,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidDigestError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidDigestErr(contentMD5 string) InvalidDigestError {
return InvalidDigestError{
APIError: GetAPIError(ErrInvalidDigest),
ContentMD5: contentMD5,
}
}
@@ -0,0 +1,53 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidLocationConstraintError is returned when an invalid location constraint is provided.
// Produces a <LocationConstraint> field in the XML response.
type InvalidLocationConstraintError struct {
APIError
LocationConstraint string
}
func (e InvalidLocationConstraintError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
LocationConstraint string
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
LocationConstraint: e.LocationConstraint,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidLocationConstraintError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidLocationConstraintErr(constraint string) InvalidLocationConstraintError {
return InvalidLocationConstraintError{
APIError: GetAPIError(ErrInvalidLocationConstraint),
LocationConstraint: constraint,
}
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import (
"encoding/xml"
)
// InvalidPartError is returned when one or more specified parts cannot be found.
// Produces <UploadId>, <PartNumber>, and <ETag> fields in the XML response.
type InvalidPartError struct {
APIError
UploadId string
PartNumber int32
ETag string
}
func (e InvalidPartError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
UploadId string `xml:",omitempty"`
PartNumber int32 `xml:",omitempty"`
ETag string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
UploadId: e.UploadId,
PartNumber: e.PartNumber,
ETag: e.ETag,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidPartError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidPartErr(uploadId string, partNumber int32, etag string) InvalidPartError {
return InvalidPartError{
APIError: GetAPIError(ErrInvalidPart),
UploadId: uploadId,
PartNumber: partNumber,
ETag: etag,
}
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidPartNumberRangeError is returned when the requested part number exceeds
// the number of parts available for the object.
// Produces <ActualPartCount> and <PartNumberRequested> fields in the XML response.
type InvalidPartNumberRangeError struct {
APIError
ActualPartCount int32
PartNumberRequested int32
}
func (e InvalidPartNumberRangeError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
ActualPartCount int32
PartNumberRequested int32
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
ActualPartCount: e.ActualPartCount,
PartNumberRequested: e.PartNumberRequested,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidPartNumberRangeError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidPartNumberRangeErr(actualPartCount, partNumberRequested int32) InvalidPartNumberRangeError {
return InvalidPartNumberRangeError{
APIError: GetAPIError(ErrInvalidPartNumberRange),
ActualPartCount: actualPartCount,
PartNumberRequested: partNumberRequested,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidRangeError is returned when the requested byte range cannot be satisfied.
// Produces <RangeRequested> and <ActualObjectSize> fields in the XML response.
type InvalidRangeError struct {
APIError
RangeRequested string
ActualObjectSize int64
}
func (e InvalidRangeError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
RangeRequested string `xml:",omitempty"`
ActualObjectSize int64 `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
RangeRequested: e.RangeRequested,
ActualObjectSize: e.ActualObjectSize,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidRangeError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetInvalidRangeErr(rangeRequested string, actualObjectSize int64) InvalidRangeError {
return InvalidRangeError{
APIError: GetAPIError(ErrInvalidRange),
RangeRequested: rangeRequested,
ActualObjectSize: actualObjectSize,
}
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// InvalidTagError is returned when a tag key or value is invalid.
// Produces <TagKey> and optionally <TagValue> fields in the XML response.
type InvalidTagError struct {
APIError
TagKey string
TagValue string
}
func (e InvalidTagError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
TagKey string `xml:",omitempty"`
TagValue string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
TagKey: e.TagKey,
TagValue: e.TagValue,
RequestID: requestID,
HostID: hostID,
})
}
func (e InvalidTagError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
// GetInvalidTagErr creates an InvalidTagError for the given error code, tag key, and optional tag value.
// code should be ErrInvalidTagKey or ErrInvalidTagValue.
func GetInvalidTagErr(code ErrorCode, tagKey, tagValue string) InvalidTagError {
return InvalidTagError{
APIError: GetAPIError(code),
TagKey: tagKey,
TagValue: tagValue,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// KeyTooLongError is returned when the object key exceeds the maximum allowed length.
// Produces <Size> and <MaxSizeAllowed> fields in the XML response.
type KeyTooLongError struct {
APIError
Size int64
MaxSizeAllowed int64
}
func (e KeyTooLongError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Size int64 `xml:",omitempty"`
MaxSizeAllowed int64 `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Size: e.Size,
MaxSizeAllowed: e.MaxSizeAllowed,
RequestID: requestID,
HostID: hostID,
})
}
func (e KeyTooLongError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetKeyTooLongErr(size, maxSizeAllowed int64) KeyTooLongError {
return KeyTooLongError{
APIError: GetAPIError(ErrKeyTooLong),
Size: size,
MaxSizeAllowed: maxSizeAllowed,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// MetadataTooLargeError is returned when the request metadata headers exceed the allowed size.
// Produces <Size> and <MaxSizeAllowed> fields in the XML response.
type MetadataTooLargeError struct {
APIError
Size int
MaxSizeAllowed int
}
func (e MetadataTooLargeError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Size int `xml:",omitempty"`
MaxSizeAllowed int `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Size: e.Size,
MaxSizeAllowed: e.MaxSizeAllowed,
RequestID: requestID,
HostID: hostID,
})
}
func (e MetadataTooLargeError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetMetadataTooLargeErr(size, maxSizeAllowed int) MetadataTooLargeError {
return MetadataTooLargeError{
APIError: GetAPIError(ErrMetadataTooLarge),
Size: size,
MaxSizeAllowed: maxSizeAllowed,
}
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import (
"encoding/xml"
"strings"
)
// MethodNotAllowedError is returned when an HTTP method is not permitted on a resource.
// Produces <Method> and <ResourceType> fields in the XML response.
// AllowedMethods is used to populate the HTTP Allow: response header.
type MethodNotAllowedError struct {
APIError
Method string
ResourceType ResourceType
AllowedMethods []string
}
func (mna *MethodNotAllowedError) AllowedMethodsString() string {
return strings.Join(mna.AllowedMethods, ", ")
}
func (e MethodNotAllowedError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Method string
ResourceType ResourceType
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Method: e.Method,
ResourceType: e.ResourceType,
RequestID: requestID,
HostID: hostID,
})
}
func (e MethodNotAllowedError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetMethodNotAllowedErr(method string, resourceType ResourceType, allowed []string) MethodNotAllowedError {
return MethodNotAllowedError{
APIError: GetAPIError(ErrMethodNotAllowed),
Method: method,
ResourceType: resourceType,
AllowedMethods: allowed,
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// NoSuchUploadError is returned when the specified multipart upload does not exist.
// Produces an <UploadId> field in the XML response.
type NoSuchUploadError struct {
APIError
UploadId string
}
func (e NoSuchUploadError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
UploadId string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
UploadId: e.UploadId,
RequestID: requestID,
HostID: hostID,
})
}
func (e NoSuchUploadError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetNoSuchUploadErr(uploadId string) NoSuchUploadError {
return NoSuchUploadError{
APIError: GetAPIError(ErrNoSuchUpload),
UploadId: uploadId,
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// NoSuchVersionError is returned when the specified version does not exist.
// Produces <Key> and <VersionId> fields in the XML response.
type NoSuchVersionError struct {
APIError
Key string
VersionId string
}
func (e NoSuchVersionError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Key string `xml:",omitempty"`
VersionId string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Key: e.Key,
VersionId: e.VersionId,
RequestID: requestID,
HostID: hostID,
})
}
func (e NoSuchVersionError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetNoSuchVersionErr(key, versionId string) NoSuchVersionError {
return NoSuchVersionError{
APIError: GetAPIError(ErrNoSuchVersion),
Key: key,
VersionId: versionId,
}
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import (
"encoding/xml"
)
type NmpAdditionalMessage string
const (
NmpAdditionalMessageIfNoneMatch NmpAdditionalMessage = "We don\\'t accept the provided value of If-None-Match header for this API"
NmpAdditionalMessageMultipleCondHeaders NmpAdditionalMessage = "Multiple conditional request headers present in the request"
)
// NotImplementedError is returned when a header implies unsupported functionality.
// Produces <Header> and <additionalMessage> fields in the XML response.
type NotImplementedError struct {
APIError
Header string
AdditionalMessage NmpAdditionalMessage
}
func (e NotImplementedError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Header string `xml:",omitempty"`
AdditionalMessage NmpAdditionalMessage `xml:"additionalMessage,omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Header: e.Header,
AdditionalMessage: e.AdditionalMessage,
RequestID: requestID,
HostID: hostID,
})
}
func (e NotImplementedError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetNotImplementedErr(header string, additionalMessage NmpAdditionalMessage) NotImplementedError {
return NotImplementedError{
APIError: GetAPIError(ErrNotImplemented),
Header: header,
AdditionalMessage: additionalMessage,
}
}
+58 -33
View File
@@ -20,43 +20,68 @@ import (
)
// Factory for building s3 object POST authentication errors.
func invalidPOSTObjectAuthErr(format string, args ...any) APIError {
return APIError{
Code: "InvalidArgument",
Description: fmt.Sprintf(format, args...),
HTTPStatusCode: http.StatusBadRequest,
func invalidPOSTObjectAuthErr(argName, argValue, format string, args ...any) S3Error {
return InvalidArgumentError{
ArgumentName: argName,
ArgumentValue: argValue,
Description: fmt.Sprintf(format, args...),
}
}
type invalidPostAuthErr struct{}
func (invalidPostAuthErr) InvalidDateFormat(s string) APIError {
func (invalidPostAuthErr) InvalidDateFormat(creds, date string) S3Error {
return invalidPOSTObjectAuthErr(
"x-amz-credential",
creds,
"incorrect date format %q. This date in the credential must be in the format \"yyyyMMdd\".",
s,
date,
)
}
func (invalidPostAuthErr) MalformedCredential() APIError {
func (invalidPostAuthErr) MalformedCredential(creds string) S3Error {
return invalidPOSTObjectAuthErr(
"x-amz-credential",
creds,
"the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".",
)
}
func (invalidPostAuthErr) IncorrectTerminal(s string) APIError {
return invalidPOSTObjectAuthErr("incorrect terminal %q. This endpoint uses \"aws4_request\".", s)
func (invalidPostAuthErr) IncorrectTerminal(creds, terminal string) S3Error {
return invalidPOSTObjectAuthErr(
"x-amz-credential",
creds,
"incorrect terminal %q. This endpoint uses \"aws4_request\".",
terminal,
)
}
func (invalidPostAuthErr) IncorrectRegion(expected, actual string) APIError {
return invalidPOSTObjectAuthErr("the region %q is wrong; expecting %q", actual, expected)
func (invalidPostAuthErr) IncorrectRegion(creds, expected, actual string) S3Error {
return invalidPOSTObjectAuthErr(
"x-amz-credential",
creds,
"the region %q is wrong; expecting %q",
actual,
expected,
)
}
func (invalidPostAuthErr) IncorrectService(s string) APIError {
return invalidPOSTObjectAuthErr("incorrect service %q. This endpoint belongs to \"s3\".", s)
func (invalidPostAuthErr) IncorrectService(creds, service string) S3Error {
return invalidPOSTObjectAuthErr(
"x-amz-credential",
creds,
"incorrect service %q. This endpoint belongs to \"s3\".",
service,
)
}
func (invalidPostAuthErr) MissingField(field string) APIError {
return invalidPOSTObjectAuthErr("Bucket POST must contain a field named '%s'. If it is specified, please check the order of the fields.", field)
func (invalidPostAuthErr) MissingField(field string) S3Error {
return invalidPOSTObjectAuthErr(
field,
"",
"Bucket POST must contain a field named '%s'. If it is specified, please check the order of the fields.",
field,
)
}
var PostAuth invalidPostAuthErr
@@ -80,71 +105,71 @@ func invalidAccordingToPolicyErr(format string, args ...any) APIError {
type invalidPolicyDocument struct{}
func (invalidPolicyDocument) EmptyPolicy() APIError {
func (invalidPolicyDocument) EmptyPolicy() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Expecting '{' but found End-of-Input")
}
func (invalidPolicyDocument) InvalidBase64Encoding() APIError {
func (invalidPolicyDocument) InvalidBase64Encoding() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: invalid Base64 encoding.")
}
func (invalidPolicyDocument) InvalidJSON() APIError {
func (invalidPolicyDocument) InvalidJSON() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid JSON.")
}
func (invalidPolicyDocument) UnexpectedField(field string) APIError {
func (invalidPolicyDocument) UnexpectedField(field string) S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Unexpected: %q", field)
}
func (invalidPolicyDocument) MissingExpiration() APIError {
func (invalidPolicyDocument) MissingExpiration() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Policy missing expiration.")
}
func (invalidPolicyDocument) InvalidExpiration(exp string) APIError {
func (invalidPolicyDocument) InvalidExpiration(exp string) S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid 'expiration' value: '%s'", exp)
}
func (invalidPolicyDocument) InvalidConditions() APIError {
func (invalidPolicyDocument) InvalidConditions() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid 'conditions' value: must be a List.")
}
func (invalidPolicyDocument) InvalidCondition() APIError {
func (invalidPolicyDocument) InvalidCondition() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid condition test: must be a List or Object.")
}
func (invalidPolicyDocument) MissingConditionOperationIdentifier() APIError {
func (invalidPolicyDocument) MissingConditionOperationIdentifier() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid Condition: missing operation identifier.")
}
func (invalidPolicyDocument) UnknownConditionOperation(op string) APIError {
func (invalidPolicyDocument) UnknownConditionOperation(op string) S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid Condition: unknown operation '%s'.", op)
}
func (invalidPolicyDocument) IncorrectConditionArgumentsNumber(op string) APIError {
func (invalidPolicyDocument) IncorrectConditionArgumentsNumber(op string) S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid %s: wrong number of arguments.", op)
}
func (invalidPolicyDocument) MissingConditions() APIError {
func (invalidPolicyDocument) MissingConditions() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Policy missing conditions.")
}
func (invalidPolicyDocument) OnePropSimpleCondition() APIError {
func (invalidPolicyDocument) OnePropSimpleCondition() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid Simple-Condition: Simple-Conditions must have exactly one property specified.")
}
func (invalidPolicyDocument) InvalidSimpleCondition() APIError {
func (invalidPolicyDocument) InvalidSimpleCondition() S3Error {
return invalidPolicyDocumentErr("Invalid Policy: Invalid Simple-Condition: value must be a string.")
}
func (invalidPolicyDocument) ConditionFailed(condition string) APIError {
func (invalidPolicyDocument) ConditionFailed(condition string) S3Error {
return invalidAccordingToPolicyErr("Invalid according to Policy: Policy Condition failed: %s", condition)
}
func (invalidPolicyDocument) ExtraInputField(field string) APIError {
func (invalidPolicyDocument) ExtraInputField(field string) S3Error {
return invalidAccordingToPolicyErr("Invalid according to Policy: Extra input fields: %s", field)
}
func (invalidPolicyDocument) PolicyExpired() APIError {
func (invalidPolicyDocument) PolicyExpired() S3Error {
return invalidAccordingToPolicyErr("Invalid according to Policy: Policy expired.")
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
type Condition string
const (
ConditionIfMatch Condition = "If-Match"
ConditionIfNoneMatch Condition = "If-None-Match"
ConditionIfUnmodifiedSince Condition = "If-Unmodified-Since"
ConditionIfMatchSize Condition = "If-Match-Size"
ConditionIfMatchInitiatedTime Condition = "If-Match-Initiated-Time"
ConditionIfMatchLastModTime Condition = "If-Match-Last-Mod-Time"
ConditionPostBucket Condition = "Bucket POST must be of the enclosure-type multipart/form-data"
)
// PreconditionFailedError is returned when a conditional request precondition is not met.
// Produces a <Condition> field in the XML response.
type PreconditionFailedError struct {
APIError
Condition Condition
}
func (e PreconditionFailedError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Condition Condition `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Condition: e.Condition,
RequestID: requestID,
HostID: hostID,
})
}
func (e PreconditionFailedError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetPreconditionFailedErr(condition Condition) PreconditionFailedError {
return PreconditionFailedError{
APIError: GetAPIError(ErrPreconditionFailed),
Condition: condition,
}
}
+15 -15
View File
@@ -20,7 +20,7 @@ import (
)
// Factory for building AuthorizationQueryParametersError errors.
func authQueryParamError(format string, args ...any) APIError {
func authQueryParamError(format string, args ...any) S3Error {
return APIError{
Code: "AuthorizationQueryParametersError",
Description: fmt.Sprintf(format, args...),
@@ -30,60 +30,60 @@ func authQueryParamError(format string, args ...any) APIError {
type queryAuthErrors struct{}
func (queryAuthErrors) UnsupportedAlgorithm() APIError {
func (queryAuthErrors) UnsupportedAlgorithm() S3Error {
return authQueryParamError(`X-Amz-Algorithm only supports "AWS4-HMAC-SHA256 and AWS4-ECDSA-P256-SHA256"`)
}
func (queryAuthErrors) MalformedCredential() APIError {
func (queryAuthErrors) MalformedCredential(_ string) S3Error {
return authQueryParamError(`Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting "<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request".`)
}
func (queryAuthErrors) IncorrectService(s string) APIError {
func (queryAuthErrors) IncorrectService(_, s string) S3Error {
return authQueryParamError(`Error parsing the X-Amz-Credential parameter; incorrect service %q. This endpoint belongs to "s3".`, s)
}
func (queryAuthErrors) IncorrectRegion(expected, actual string) APIError {
func (queryAuthErrors) IncorrectRegion(expected, actual string) S3Error {
return authQueryParamError(`Error parsing the X-Amz-Credential parameter; the region %q is wrong; expecting %q`, actual, expected)
}
func (queryAuthErrors) IncorrectTerminal(s string) APIError {
func (queryAuthErrors) IncorrectTerminal(_, s string) S3Error {
return authQueryParamError(`Error parsing the X-Amz-Credential parameter; incorrect terminal %q. This endpoint uses "aws4_request".`, s)
}
func (queryAuthErrors) InvalidDateFormat(s string) APIError {
func (queryAuthErrors) InvalidDateFormat(_, s string) S3Error {
return authQueryParamError(`Error parsing the X-Amz-Credential parameter; incorrect date format %q. This date in the credential must be in the format "yyyyMMdd".`, s)
}
func (queryAuthErrors) DateMismatch(expected, actual string) APIError {
func (queryAuthErrors) DateMismatch(expected, actual string) S3Error {
return authQueryParamError(`Invalid credential date %q. This date is not the same as X-Amz-Date: %q.`, expected, actual)
}
func (queryAuthErrors) ExpiresTooLarge() APIError {
func (queryAuthErrors) ExpiresTooLarge() S3Error {
return authQueryParamError("X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds")
}
func (queryAuthErrors) ExpiresNegative() APIError {
func (queryAuthErrors) ExpiresNegative() S3Error {
return authQueryParamError("X-Amz-Expires must be non-negative")
}
func (queryAuthErrors) ExpiresNumber() APIError {
func (queryAuthErrors) ExpiresNumber() S3Error {
return authQueryParamError("X-Amz-Expires should be a number")
}
func (queryAuthErrors) MissingRequiredParams() APIError {
func (queryAuthErrors) MissingRequiredParams() S3Error {
return authQueryParamError("Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.")
}
func (queryAuthErrors) InvalidXAmzDateFormat() APIError {
func (queryAuthErrors) InvalidXAmzDateFormat() S3Error {
return authQueryParamError(`X-Amz-Date must be in the ISO8601 Long Format "yyyyMMdd'T'HHmmss'Z'"`)
}
// a custom non-AWS error
func (queryAuthErrors) OnlyHMACSupported() APIError {
func (queryAuthErrors) OnlyHMACSupported() S3Error {
return authQueryParamError("X-Amz-Algorithm only supports \"AWS4-HMAC-SHA256\"")
}
func (queryAuthErrors) SecurityTokenNotSupported() APIError {
func (queryAuthErrors) SecurityTokenNotSupported() S3Error {
return authQueryParamError("Authorization with X-Amz-Security-Token is not supported")
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import (
"encoding/xml"
)
// RequestTimeTooSkewedError is returned when the request timestamp is too far from server time.
// Produces <RequestTime>, <ServerTime>, and <MaxAllowedSkewMilliseconds> fields.
type RequestTimeTooSkewedError struct {
APIError
RequestTime string
ServerTime string
MaxAllowedSkewMilliseconds int
}
func (e RequestTimeTooSkewedError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
RequestTime string
ServerTime string
MaxAllowedSkewMilliseconds int
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
RequestTime: e.RequestTime,
ServerTime: e.ServerTime,
MaxAllowedSkewMilliseconds: e.MaxAllowedSkewMilliseconds,
RequestID: requestID,
HostID: hostID,
})
}
func (e RequestTimeTooSkewedError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetRequestTimeTooSkewedErr(requestTime, serverTime string, maxAllowedMilliseconds int) RequestTimeTooSkewedError {
return RequestTimeTooSkewedError{
APIError: GetAPIError(ErrRequestTimeTooSkewed),
RequestTime: requestTime,
ServerTime: serverTime,
MaxAllowedSkewMilliseconds: maxAllowedMilliseconds,
}
}
+38 -267
View File
@@ -24,6 +24,15 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3/types"
)
// S3Error is the interface implemented by all S3 error types.
// It allows centralized error handling while supporting per-error-type XML fields.
type S3Error interface {
error
StatusCode() int
BaseError() APIError
XMLBody(requestID, hostID string) []byte
}
// APIError structure
type APIError struct {
Code string
@@ -31,17 +40,8 @@ type APIError struct {
HTTPStatusCode int
}
// APIErrorResponse - error response format
type APIErrorResponse struct {
XMLName xml.Name `xml:"Error" json:"-"`
Code string
Message string
Key string `xml:"Key,omitempty" json:"Key,omitempty"`
BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"`
Resource string
Region string `xml:"Region,omitempty" json:"Region,omitempty"`
RequestID string `xml:"RequestId" json:"RequestId"`
HostID string `xml:"HostId" json:"HostId"`
func (e APIError) BaseError() APIError {
return e
}
func (A APIError) Error() string {
@@ -52,6 +52,23 @@ func (A APIError) Error() string {
return bytesBuffer.String()
}
func (e APIError) StatusCode() int { return e.HTTPStatusCode }
func (e APIError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
RequestID: requestID,
HostID: hostID,
})
}
// ErrorCode type of error status.
type ErrorCode int
@@ -76,48 +93,28 @@ const (
ErrInvalidBucketName
ErrInvalidDigest
ErrBadDigest
ErrInvalidMaxBuckets
ErrNegativeMaxKeys
ErrInvalidObjectAttributes
ErrInvalidPart
ErrInvalidPartNumber
ErrInvalidPartNumberRange
ErrRangeAndPartNumber
ErrInvalidPartOrder
ErrInvalidCompleteMpPartNumber
ErrInternalError
ErrNonEmptyRequestBody
ErrIncompleteBody
ErrInvalidCopyDest
ErrInvalidCopySourceRange
ErrInvalidCopySourceBucket
ErrInvalidCopySourceObject
ErrInvalidCopySourceEncoding
ErrInvalidTagKey
ErrInvalidTagValue
ErrDuplicateTagKey
ErrBucketTaggingLimited
ErrObjectTaggingLimited
ErrCannotParseHTTPRequest
ErrInvalidURLEncodedTagging
ErrInvalidAuthHeader
ErrUnsupportedAuthorizationType
ErrMalformedPOSTRequest
ErrPOSTFileRequired
ErrPostPolicyConditionInvalidFormat
ErrEntityTooSmall
ErrEntityTooLarge
ErrMissingFields
ErrMissingCredTag
ErrMalformedXML
ErrMalformedCredentialDate
ErrMissingSignHeadersTag
ErrMissingSignTag
ErrUnsignedHeaders
ErrExpiredPresignRequest
ErrSignatureDoesNotMatch
ErrContentSHA256Mismatch
ErrInvalidSHA256Paylod
ErrInvalidSHA256PayloadUsage
ErrUnsupportedAnonymousSignedStreaming
ErrMissingContentLength
@@ -127,7 +124,6 @@ const (
ErrMissingDateHeader
ErrGetUploadsWithKey
ErrVersionsWithKey
ErrCopySourceNotAllowed
ErrInvalidRequest
ErrAuthNotSetup
ErrNotImplemented
@@ -141,14 +137,8 @@ const (
ErrMissingObjectLockConfigurationNoSpaces
ErrObjectLockConfigurationNotAllowed
ErrObjectLocked
ErrInvalidRetainUntilDate
ErrPastObjectLockRetainDate
ErrObjectLockInvalidRetentionPeriod
ErrInvalidLegalHoldStatus
ErrInvalidObjectLockMode
ErrNoSuchBucketPolicy
ErrBucketTaggingNotFound
ErrObjectLockInvalidHeaders
ErrObjectAttributesInvalidHeader
ErrRequestTimeTooSkewed
ErrInvalidBucketAclWithObjectOwnership
@@ -158,10 +148,7 @@ const (
ErrMalformedACL
ErrUnexpectedContent
ErrMissingSecurityHeader
ErrInvalidMetadataDirective
ErrInvalidTaggingDirective
ErrKeyTooLong
ErrInvalidVersionId
ErrNoSuchVersion
ErrSuspendedVersioningNotAllowed
ErrMissingRequestBody
@@ -170,26 +157,20 @@ const (
ErrChecksumRequired
ErrMissingContentSha256
ErrInvalidChecksumAlgorithm
ErrInvalidChecksumPart
ErrChecksumTypeWithAlgo
ErrInvalidChecksumHeader
ErrTrailerHeaderNotSupported
ErrBadRequest
ErrMissingUploadId
ErrInvalidUploadIdMarker
ErrNoSuchCORSConfiguration
ErrCORSForbidden
ErrMissingCORSOrigin
ErrCORSIsNotEnabled
ErrNotModified
ErrInvalidLocationConstraint
ErrInvalidArgument
ErrMalformedTrailer
ErrInvalidChunkSize
ErrSlowDown
ErrMetadataTooLarge
ErrOnlyAws4HmacSha256
ErrInvalidDateHeader
ErrUnsupportedAuthorizationMechanism
// Non-AWS errors
@@ -288,21 +269,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The Content-MD5 you specified did not match what we received.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidMaxBuckets: {
Code: "InvalidArgument",
Description: "Argument max-buckets must be an integer between 1 and 10000.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrNegativeMaxKeys: {
Code: "InvalidArgument",
Description: "max-keys cannot be negative",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidObjectAttributes: {
Code: "InvalidArgument",
Description: "Invalid attribute name specified.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrNoSuchBucket: {
Code: "NoSuchBucket",
Description: "The specified bucket does not exist.",
@@ -338,11 +304,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPartNumber: {
Code: "InvalidArgument",
Description: "Part number must be an integer between 1 and 10000, inclusive.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidPartNumberRange: {
Code: "InvalidPartNumber",
Description: "The requested partnumber is not satisfiable.",
@@ -358,36 +319,11 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The list of parts was not in ascending order. Parts must be ordered by part number.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCompleteMpPartNumber: {
Code: "InvalidArgument",
Description: "PartNumber must be >= 1",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopyDest: {
Code: "InvalidRequest",
Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopySourceRange: {
Code: "InvalidArgument",
Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopySourceBucket: {
Code: "InvalidArgument",
Description: "Invalid copy source bucket name",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopySourceObject: {
Code: "InvalidArgument",
Description: "Invalid copy source object key",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidCopySourceEncoding: {
Code: "InvalidArgument",
Description: "Invalid copy source encoding",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidTagKey: {
Code: "InvalidTag",
Description: "The TagKey you have provided is invalid",
@@ -418,41 +354,16 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "An error occurred when parsing the HTTP request.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidURLEncodedTagging: {
Code: "InvalidArgument",
Description: "The header 'x-amz-tagging' shall be encoded as UTF-8 then URLEncoded URL query parameters without tag name duplicates.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMalformedXML: {
Code: "MalformedXML",
Description: "The XML you provided was not well-formed or did not validate against our published schema.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidAuthHeader: {
Code: "InvalidArgument",
Description: "Authorization header is invalid -- one and only one ' ' (space) required.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedAuthorizationType: {
Code: "InvalidArgument",
Description: "Unsupported Authorization Type",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMalformedPOSTRequest: {
Code: "MalformedPOSTRequest",
Description: "The body of your POST request is not well-formed multipart/form-data.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPOSTFileRequired: {
Code: "InvalidArgument",
Description: "POST requires exactly one file upload per request.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPostPolicyConditionInvalidFormat: {
Code: "PostPolicyInvalidKeyName",
Description: "Invalid according to Policy: Policy Condition failed.",
HTTPStatusCode: http.StatusForbidden,
},
ErrEntityTooSmall: {
Code: "EntityTooSmall",
Description: "Your proposed upload is smaller than the minimum allowed size",
@@ -463,31 +374,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Your proposed upload exceeds the maximum allowed object size.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingFields: {
Code: "MissingFields",
Description: "Missing fields in request.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingCredTag: {
Code: "InvalidRequest",
Description: "Missing Credential field for this request.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingSignHeadersTag: {
Code: "InvalidArgument",
Description: "Signature header missing SignedHeaders field.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingSignTag: {
Code: "AccessDenied",
Description: "Signature header missing Signature field.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsignedHeaders: {
Code: "AccessDenied",
Description: "There were headers present in the request which were not signed.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrExpiredPresignRequest: {
Code: "AccessDenied",
Description: "Request has expired.",
@@ -513,11 +399,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidSHA256Paylod: {
Code: "InvalidArgument",
Description: "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-UNSIGNED-PAYLOAD-TRAILER, STREAMING-AWS4-HMAC-SHA256-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD, STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER or a valid sha256 value.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidSHA256PayloadUsage: {
Code: "InvalidRequest",
Description: "The value of x-amz-content-sha256 header is invalid.",
@@ -553,11 +434,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "There is no such thing as the ?versions sub-resource for a key",
HTTPStatusCode: http.StatusBadRequest,
},
ErrCopySourceNotAllowed: {
Code: "InvalidArgument",
Description: "You can only specify a copy source header for copy requests.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidRequest: {
Code: "InvalidRequest",
Description: "Invalid Request.",
@@ -623,31 +499,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Access Denied because object protected by object lock.",
HTTPStatusCode: http.StatusForbidden,
},
ErrInvalidRetainUntilDate: {
Code: "InvalidArgument",
Description: "The retain until date must be provided in ISO 8601 format",
HTTPStatusCode: http.StatusBadRequest,
},
ErrPastObjectLockRetainDate: {
Code: "InvalidArgument",
Description: "The retain until date must be in the future!",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectLockInvalidRetentionPeriod: {
Code: "InvalidArgument",
Description: "Default retention period must be a positive integer value.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidLegalHoldStatus: {
Code: "InvalidArgument",
Description: "Legal Hold must be either of 'ON' or 'OFF'",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidObjectLockMode: {
Code: "InvalidArgument",
Description: "Unknown wormMode directive.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrNoSuchBucketPolicy: {
Code: "NoSuchBucketPolicy",
Description: "The bucket policy does not exist.",
@@ -658,11 +509,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The TagSet does not exist.",
HTTPStatusCode: http.StatusNotFound,
},
ErrObjectLockInvalidHeaders: {
Code: "InvalidRequest",
Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrObjectAttributesInvalidHeader: {
Code: "InvalidRequest",
Description: "The x-amz-object-attributes header specifying the attributes to be retrieved is either missing or empty",
@@ -708,21 +554,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Your request was missing a required header.",
HTTPStatusCode: http.StatusNotFound,
},
ErrInvalidMetadataDirective: {
Code: "InvalidArgument",
Description: "Unknown metadata directive.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidTaggingDirective: {
Code: "InvalidArgument",
Description: "Unknown tagging directive.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidVersionId: {
Code: "InvalidArgument",
Description: "Invalid version id specified",
HTTPStatusCode: http.StatusBadRequest,
},
ErrKeyTooLong: {
Code: "KeyTooLongError",
Description: "Your key is too long.",
@@ -768,11 +599,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Checksum algorithm provided is unsupported. Please try again with any of the valid types: [CRC32, CRC32C, CRC64NVME, MD5, SHA1, SHA256, SHA512, XXHASH128, XXHASH3, XXHASH64]",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidChecksumPart: {
Code: "InvalidArgument",
Description: "Invalid Base64 or multiple checksums present in request",
HTTPStatusCode: http.StatusBadRequest,
},
ErrChecksumTypeWithAlgo: {
Code: "InvalidRequest",
Description: "The x-amz-checksum-type header can only be used with the x-amz-checksum-algorithm header.",
@@ -793,16 +619,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "Bad Request",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingUploadId: {
Code: "InvalidArgument",
Description: "This operation does not accept partNumber without uploadId",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidUploadIdMarker: {
Code: "InvalidArgument",
Description: "Invalid uploadId marker",
HTTPStatusCode: http.StatusBadRequest,
},
ErrNoSuchCORSConfiguration: {
Code: "NoSuchCORSConfiguration",
Description: "The CORS configuration does not exist",
@@ -833,11 +649,6 @@ var errorCodeResponse = map[ErrorCode]APIError{
Description: "The specified location-constraint is not valid",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidArgument: {
Code: "InvalidArgument",
Description: "",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMalformedTrailer: {
Code: "MalformedTrailerError",
Description: "The request contained trailing data that was not well-formed or did not conform to our published schema.",
@@ -854,21 +665,10 @@ var errorCodeResponse = map[ErrorCode]APIError{
HTTPStatusCode: http.StatusServiceUnavailable,
},
ErrMetadataTooLarge: {
// TODO: should have 'Size' and 'MaxSizeAllowed' properties
Code: "MetadataTooLarge",
Description: "Your metadata headers exceed the maximum allowed metadata size",
HTTPStatusCode: http.StatusBadRequest,
},
ErrOnlyAws4HmacSha256: {
Code: "InvalidArgument",
Description: "Only AWS4-HMAC-SHA256 is supported",
HTTPStatusCode: http.StatusBadRequest,
},
ErrInvalidDateHeader: {
Code: "InvalidArgument",
Description: "X-Amz-Date must be formated via ISO8601 Long format",
HTTPStatusCode: http.StatusBadRequest,
},
ErrUnsupportedAuthorizationMechanism: {
Code: "InvalidRequest",
Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.",
@@ -960,21 +760,6 @@ func GetAPIError(code ErrorCode) APIError {
return errorCodeResponse[code]
}
// getErrorResponse gets in standard error and resource value and
// provides a encodable populated response values
func GetAPIErrorResponse(err APIError, resource, requestID, hostID string) []byte {
return encodeResponse(APIErrorResponse{
Code: err.Code,
Message: err.Description,
BucketName: "",
Key: "",
Resource: resource,
Region: "",
RequestID: requestID,
HostID: hostID,
})
}
// Encodes the response headers into XML format.
func encodeResponse(response any) []byte {
var bytesBuffer bytes.Buffer
@@ -1061,14 +846,6 @@ func GetInvalidMpObjectSizeErr(val string) APIError {
}
}
func CreateExceedingRangeErr(objSize int64) APIError {
return APIError{
Code: "InvalidArgument",
Description: fmt.Sprintf("Range specified is not valid for source object of size: %d", objSize),
HTTPStatusCode: http.StatusBadRequest,
}
}
func GetInvalidCORSHeaderErr(header string) APIError {
return APIError{
Code: "InvalidRequest",
@@ -1109,22 +886,6 @@ func GetInvalidCORSMethodErr(method string) APIError {
}
}
func GetInvalidMaxLimiterErr(limiter string) APIError {
return APIError{
Code: "InvalidArgument",
Description: fmt.Sprintf("Provided %s not an integer or within integer range", limiter),
HTTPStatusCode: http.StatusBadRequest,
}
}
func GetNegativeMaxLimiterErr(limiter string) APIError {
return APIError{
Code: "InvalidArgument",
Description: fmt.Sprintf("Argument %s must be an integer between 0 and 2147483647", limiter),
HTTPStatusCode: http.StatusBadRequest,
}
}
func GetCopySourceObjectTooLargeErr(limit int64) APIError {
return APIError{
Code: "InvalidRequest",
@@ -1132,3 +893,13 @@ func GetCopySourceObjectTooLargeErr(limit int64) APIError {
HTTPStatusCode: http.StatusBadRequest,
}
}
type ResourceType string
const (
ResourceTypeBucket ResourceType = "BUCKET"
ResourceTypeObject ResourceType = "OBJECT"
ResourceTypeService ResourceType = "SERVICE"
ResourceTypeBucketPolicy ResourceType = "BUCKETPOLICY"
ResourceTypeUpload ResourceType = "UPLOAD"
)
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2026 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package s3err
import "encoding/xml"
// SignatureDoesNotMatchError is returned when request signature verification fails.
// Produces diagnostic fields to help callers debug the mismatch.
type SignatureDoesNotMatchError struct {
APIError
AWSAccessKeyId string
StringToSign string
SignatureProvided string
StringToSignBytes string
CanonicalRequest string
CanonicalRequestBytes string
}
func (e SignatureDoesNotMatchError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
AWSAccessKeyId string `xml:",omitempty"`
StringToSign string `xml:",omitempty"`
SignatureProvided string `xml:",omitempty"`
StringToSignBytes string `xml:",omitempty"`
CanonicalRequest string `xml:",omitempty"`
CanonicalRequestBytes string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
AWSAccessKeyId: e.AWSAccessKeyId,
StringToSign: e.StringToSign,
SignatureProvided: e.SignatureProvided,
StringToSignBytes: e.StringToSignBytes,
CanonicalRequest: e.CanonicalRequest,
CanonicalRequestBytes: e.CanonicalRequestBytes,
RequestID: requestID,
HostID: hostID,
})
}
func (e SignatureDoesNotMatchError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
func GetSignatureDoesNotMatchErr(accessKeyId, stringToSign, signatureProvided, stringToSignBytes, canonicalRequest, canonicalRequestBytes string) SignatureDoesNotMatchError {
return SignatureDoesNotMatchError{
APIError: GetAPIError(ErrSignatureDoesNotMatch),
AWSAccessKeyId: accessKeyId,
StringToSign: stringToSign,
SignatureProvided: signatureProvided,
StringToSignBytes: stringToSignBytes,
CanonicalRequest: canonicalRequest,
CanonicalRequestBytes: canonicalRequestBytes,
}
}
+51 -17
View File
@@ -15,69 +15,103 @@
package s3err
import (
"encoding/xml"
"fmt"
"net/http"
)
// MalformedAuthError is returned when a Signature V4 authorization header is malformed.
// Produces a <Region> field in the XML response when the expected gateway region is known.
type MalformedAuthError struct {
APIError
Region string
}
func (e MalformedAuthError) XMLBody(requestID, hostID string) []byte {
return encodeResponse(struct {
XMLName xml.Name `xml:"Error"`
Code string
Message string
Region string `xml:",omitempty"`
RequestID string `xml:"RequestId,omitempty"`
HostID string `xml:"HostId,omitempty"`
}{
Code: e.Code,
Message: e.Description,
Region: e.Region,
RequestID: requestID,
HostID: hostID,
})
}
func (e MalformedAuthError) Is(target error) bool {
t, ok := target.(APIError)
return ok && e.APIError == t
}
// Factory for building AuthorizationHeaderMalformed errors.
func malformedAuthError(format string, args ...any) APIError {
return APIError{
Code: "AuthorizationHeaderMalformed",
Description: fmt.Sprintf("The authorization header is malformed; %s", fmt.Sprintf(format, args...)),
HTTPStatusCode: http.StatusBadRequest,
func malformedAuthError(format string, args ...any) MalformedAuthError {
return MalformedAuthError{
APIError: APIError{
Code: "AuthorizationHeaderMalformed",
Description: fmt.Sprintf("The authorization header is malformed; %s", fmt.Sprintf(format, args...)),
HTTPStatusCode: http.StatusBadRequest,
},
}
}
type malformedAuthErrors struct{}
func (malformedAuthErrors) InvalidDateFormat(s string) APIError {
func (malformedAuthErrors) InvalidDateFormat(_, s string) S3Error {
return malformedAuthError(
"incorrect date format %q. This date in the credential must be in the format \"yyyyMMdd\".",
s,
)
}
func (malformedAuthErrors) MalformedCredential() APIError {
func (malformedAuthErrors) MalformedCredential(_ string) S3Error {
return malformedAuthError(
"the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".",
)
}
func (malformedAuthErrors) MissingCredential() APIError {
func (malformedAuthErrors) MissingCredential() S3Error {
return malformedAuthError("missing Credential.")
}
func (malformedAuthErrors) MissingSignature() APIError {
func (malformedAuthErrors) MissingSignature() S3Error {
return malformedAuthError("missing Signature.")
}
func (malformedAuthErrors) MissingSignedHeaders() APIError {
func (malformedAuthErrors) MissingSignedHeaders() S3Error {
return malformedAuthError("missing SignedHeaders.")
}
func (malformedAuthErrors) IncorrectTerminal(s string) APIError {
func (malformedAuthErrors) IncorrectTerminal(_, s string) S3Error {
return malformedAuthError("incorrect terminal %q. This endpoint uses \"aws4_request\".", s)
}
func (malformedAuthErrors) IncorrectRegion(expected, actual string) APIError {
return malformedAuthError("the region %q is wrong; expecting %q", actual, expected)
func (malformedAuthErrors) IncorrectRegion(expected, actual string) S3Error {
err := malformedAuthError("the region %q is wrong; expecting %q", actual, expected)
err.Region = expected
return err
}
func (malformedAuthErrors) IncorrectService(s string) APIError {
func (malformedAuthErrors) IncorrectService(_, s string) S3Error {
return malformedAuthError("incorrect service %q. This endpoint belongs to \"s3\".", s)
}
func (malformedAuthErrors) MalformedComponent(s string) APIError {
func (malformedAuthErrors) MalformedComponent(s string) S3Error {
return malformedAuthError("the authorization component %q is malformed.", s)
}
func (malformedAuthErrors) MissingComponents() APIError {
func (malformedAuthErrors) MissingComponents() S3Error {
return malformedAuthError(
"the authorization header requires three components: Credential, SignedHeaders, and Signature.",
)
}
func (malformedAuthErrors) DateMismatch() APIError {
func (malformedAuthErrors) DateMismatch() S3Error {
return malformedAuthError(
"The authorization header is malformed; Invalid credential date. Date is not the same as X-Amz-Date.",
)