Repository: minio/minio (language: go)

Issue:
Incompatibility with S3 behavior when using the AWS CRT-based S3 java client for downloading empty files with `checksumValidation` disabled
## NOTE
If this case is urgent, please subscribe to [Subnet](https://min.io/pricing) so that our 24/7 support team may help you faster.

<!--- Provide a general summary of the issue in the Title above -->

## Expected Behavior
<!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work -->

The same behavior of the AWS CRT-based S3 with real S3 endpoint which is to return a `200` status code with `contentLength` equal to 0. Please note that this is what minio (and S3) would return with the standard `S3AsyncClient` java client from the v2 sdk.

## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior -->

The following exception is thrown (status code: 416):
```
software.amazon.awssdk.services.s3.model.S3Exception: The requested range is not satisfiable (Service: S3, Status Code: 416, Request ID: ***, Extended Request ID: ***)
```

## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change -->
Probably this has to do with the range header being passed where it shouldn't because the CRT client is confused by the `xl.meta` file or some minio specific file metadata? 

Update: This only happens when `checksumValidation` on the SDK is disabled 

## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
<!--- and make sure you have followed https://github.com/minio/minio/tree/release/docs/debugging to capture relevant logs -->

```java
S3AsyncClient s3CrtAsync = S3AsyncClient.crtBuilder().checksumValidationEnabled(false).build(); // using s3 CRT version 0.29.18
s3CrtAsync.putObject(r -> r.bucket("bucket").key("test"), AsyncRequestBody.empty()).join();
s3CrtAsync.getObject(r -> r.bucket("bucket").key("test"), Path.of("/tmp/test")).join(); // throws range is not satisfiable exception
```

## Context
<!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
Trying to upgrade from the standard S3 client to the CRT client to enhance performance of download operations, some of our tests that expect empty files started to fail. For more about the CRT client, see: https://aws.amazon.com/blogs/developer/introducing-crt-based-s3-client-and-the-s3-transfer-manager-in-the-aws-sdk-for-java-2-x/

## Regression
<!-- Is this issue a regression? (Yes / No) --> No
<!-- If Yes, optionally please include minio version or commit id or PR# that caused this regression, if you have these details. -->

## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used (`minio --version`):
minio version RELEASE.2024-01-29T03-56-32Z (commit-id=9987ff570bcea7c26a8faec32910f10f49576d0c)
Runtime: go1.21.6 linux/arm64
License: GNU AGPLv3 <https://www.gnu.org/licenses/agpl-3.0.html>
Copyright: 2015-2024 MinIO, Inc.

* Server setup and configuration: Docker image on Mac with vanilla config

* Operating System and version (`uname -a`):
Linux a7c7d12c247e 6.5.11-linuxkit #1 SMP PREEMPT Mon Dec  4 11:30:00 UTC 2023 aarch64 aarch64 aarch64 GNU/Linux



Discussion / hints:
First step

- upgrade your cluster
- collect `mc admin trace -v alias/ `
@harshavardhana  I upgraded to `RELEASE.2024-05-01T01-11-10Z` and captured the trace ([RELEASE.2024-05-01T01-11-10Z.trace.txt](https://github.com/minio/minio/files/15182561/RELEASE.2024-05-01T01-11-10Z.trace.txt)).

I added one step to the repro steps which is to disable checksum validation. If it is enabled (which is the default behavior of AWS Sdk) the download would go through.
Do you have a self contained reproducer? 
Okay from the trace MinIO is returning correct error you have uploaded a zero-byte object and we are returning an error.

```
127.0.0.1:9000 [REQUEST s3.PutObject] [2024-05-02T00:34:57.311] [Client IP: 192.168.65.1]
127.0.0.1:9000 PUT /a98c4bd72-cdf5-4273-a4ac-2177dd6eee24/my-key
127.0.0.1:9000 Proto: HTTP/1.1
127.0.0.1:9000 Host: 127.0.0.1:9000
127.0.0.1:9000 Expect: 100-continue
127.0.0.1:9000 User-Agent: aws-sdk-java/2.25.40 Mac_OS_X/14.0 Java_HotSpot_TM__64-Bit_Server_VM/11.0.20+9-LTS-256 Java/11.0.20 kotlin/1.4.10-release-411 (1.4.10) vend
or/Oracle_Corporation io/async http/s3crt cfg/retry-mode/legacy cfg/auth-source#stat CRTS3NativeClient/0.1.x platform/unknown
127.0.0.1:9000 X-Amz-Date: 20240502T003457Z
127.0.0.1:9000 Amz-Sdk-Invocation-Id: fbda1cfa-68f6-5899-ce23-98948579d21e
127.0.0.1:9000 Amz-Sdk-Request: attempt=1; max=1
127.0.0.1:9000 Authorization: AWS4-HMAC-SHA256 Credential=ROOTNAME/20240502/us-west-2/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-len
gth;content-type;host;x-amz-content-sha256;x-amz-date, Signature=ddd41556b551943b62898fde9c4a9937c7998258ffef1cffbaf0f951abdf4c43
127.0.0.1:9000 Content-Length: 0
127.0.0.1:9000 Content-Type: application/octet-stream
127.0.0.1:9000 X-Amz-Content-Sha256: UNSIGNED-PAYLOAD
127.0.0.1:9000 <BLOB>
127.0.0.1:9000 [RESPONSE] [2024-05-02T00:34:57.314] [ Duration 3.043ms TTFB 0s ↑ 135 B  ↓ 0 B ]
127.0.0.1:9000 200 OK
127.0.0.1:9000 Accept-Ranges: bytes
127.0.0.1:9000 ETag: "d41d8cd98f00b204e9800998ecf8427e"
127.0.0.1:9000 Server: MinIO
127.0.0.1:9000 Strict-Transport-Security: max-age=31536000; includeSubDomains
127.0.0.1:9000 Vary: Origin,Accept-Encoding
127.0.0.1:9000 X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
127.0.0.1:9000 X-Amz-Request-Id: 17CB84CC92937651
127.0.0.1:9000 X-Content-Type-Options: nosniff
127.0.0.1:9000 Content-Length: 0
127.0.0.1:9000 X-Xss-Protection: 1; mode=block
127.0.0.1:9000 <BLOB>
```

```
127.0.0.1:9000 [REQUEST s3.GetObject] [2024-05-02T00:34:57.327] [Client IP: 192.168.65.1]
127.0.0.1:9000 GET /a98c4bd72-cdf5-4273-a4ac-2177dd6eee24/my-key
127.0.0.1:9000 Proto: HTTP/1.1
127.0.0.1:9000 Host: 127.0.0.1:9000
127.0.0.1:9000 Range: bytes=0-8388607
127.0.0.1:9000 User-Agent: aws-sdk-java/2.25.40 Mac_OS_X/14.0 Java_HotSpot_TM__64-Bit_Server_VM/11.0.20+9-LTS-256 Java/11.0.20 kotlin/1.4.10-release-411 (1.4.10) vendor/Oracle_Corporation io/async http/s3crt cfg/retry-mode/legacy cfg/auth-source#stat CRTS3NativeClient/0.1.x platform/unknown
127.0.0.1:9000 X-Amz-Content-Sha256: UNSIGNED-PAYLOAD
127.0.0.1:9000 Amz-Sdk-Invocation-Id: b5aa8770-b047-fedc-7075-a9e995fa31a7
127.0.0.1:9000 Amz-Sdk-Request: attempt=1; max=1
127.0.0.1:9000 Authorization: AWS4-HMAC-SHA256 Credential=ROOTNAME/20240502/us-west-2/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;host;range;x-amz-content-sha256;x-amz-date, Signature=7d53da56d3056a33da0747963681f9c071cc87e96e4d5f51db5107189753fd28
127.0.0.1:9000 Content-Length: 0
127.0.0.1:9000 X-Amz-Date: 20240502T003457Z
127.0.0.1:9000 <BLOB>
127.0.0.1:9000 [RESPONSE] [2024-05-02T00:34:57.328] [ Duration 656µs TTFB 571.208µs ↑ 121 B  ↓ 401 B ]
127.0.0.1:9000 416 Requested Range Not Satisfiable
127.0.0.1:9000 X-Content-Type-Options: nosniff
127.0.0.1:9000 Accept-Ranges: bytes
127.0.0.1:9000 Content-Length: 401
127.0.0.1:9000 Content-Type: application/xml
127.0.0.1:9000 ETag: "d41d8cd98f00b204e9800998ecf8427e"
127.0.0.1:9000 Server: MinIO
127.0.0.1:9000 Strict-Transport-Security: max-age=31536000; includeSubDomains
127.0.0.1:9000 Vary: Origin,Accept-Encoding
127.0.0.1:9000 Last-Modified: Thu, 02 May 2024 00:34:57 GMT
127.0.0.1:9000 X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
127.0.0.1:9000 X-Amz-Request-Id: 17CB84CC9384B7B6
127.0.0.1:9000 X-Xss-Protection: 1; mode=block
127.0.0.1:9000 <?xml version="1.0" encoding="UTF-8"?>
<Error><Code>InvalidRange</Code><Message>The requested range is not satisfiable</Message><Key>my-key</Key><BucketName>a98c4bd72-cdf5-4273-a4ac-2177dd6eee24</BucketName><Resource>/a98c4bd72-cdf5-4273-a4ac-2177dd6eee24/my-key</Resource><RequestId>17CB84CC9384B7B6</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
```

Look at the incorrect range that was specified 

```
127.0.0.1:9000 Range: bytes=0-8388607
```

Tested this with AWS S3 with zero byte object

```
mc cp body s3/vadmeste/zero-bytes/
...thub.com/minio/minio/body: 0 B / ? ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓┃
```

```
curl -H "Range: bytes=0-9" -I https://s3.amazonaws.com/vadmeste/zero-bytes/body
HTTP/1.1 416 Requested Range Not Satisfiable
x-amz-request-id: K6EV38GT1NKP6DYQ
x-amz-id-2: o1Ru2V3NWCImbRm3N74rtEaxcT+X461M8TDxpqjfMl5bHadVyuGt+pox49ar+KWf0V6Lr27yYnM=
Content-Type: application/xml
Date: Thu, 02 May 2024 11:01:58 GMT
Server: AmazonS3
```

```
curl -H "Range: bytes=0-9" https://s3.amazonaws.com/vadmeste/zero-bytes/body
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>InvalidRange</Code><Message>The requested range is not satisfiable</Message><RangeRequested>bytes=0-9</RangeRequested><ActualObjectSize>0</ActualObjectSize><RequestId>7QQ7F9DRRS8365A4</RequestId><HostId>p73Q7Ei9b5MpK0XWwooFdkgzkIkMOftkzYCG/z4PubPGszJUiok/5RcZCEBekGZVS+T3QJX+iFk=</HostId></Error>
```

Not sure where you are testing this against - I don't see this with AWS S3. MinIO is behaving correctly for the API calls as expected.

Closing this issue for now. 
@harshavardhana 
> Not sure where you are testing this against 

I'm running a Java app locally against minio running locally in Docker (fails) and against s3 (succeeds)

> Do you have a self contained reproducer?

Here: [minio-crt.zip](https://github.com/minio/minio/files/15192909/minio-crt.zip)


In oder to run, extract the zip file then:
```
mvn package
java -jar target/minio-crt-1.0-SNAPSHOT.jar <access_key> <secret_key> <endpoint>
```

The s3 run will pass. e.g.: 
```
java -jar target/minio-crt-1.0-SNAPSHOT.jar s3-access-key s3-secret-key https://s3.us-west-2.amazonaws.com

File with 0 contentLength download successfully: /tmp/baafee96-d47e-48c3-86e7-e7425a8c6568
```

The minio run will fail. e.g:
```
java -jar target/minio-crt-1.0-SNAPSHOT.jar minio-access-key minio-secret-key http://127.0.0.1:9000

Exception in thread "main" java.util.concurrent.CompletionException: software.amazon.awssdk.services.s3.model.S3Exception: The requested range is not satisfiable (Service: S3, Status Code: 416, Request ID: 17CBC06EFF2602D7, Extended Request ID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8)
```

Again as descried in this ticket, the issue happens when using the S3 CRT client. I looked more closely at the sdk traces themselves. It seems the CRT client first tries to set range, and then it fails because the content length is 0 as you mentioned, but then in the S3 case, it is able to detect that error and falls-back to retrying the request _without_ the range header. However in minio case, the sdk is behaving differently which indicates the minio server error is (slightly?) different from what S3 returns. 

Here is the sdk trace when using S3, notice the line `Detected empty file with request 0x14bf45f40. Sending new request without range header`
```
[2024-05-02T17:51:54Z] [000000016fe77000] [http-stream] - id=0x14bf89820: Client request complete, response status: 416 (Range Not Satisfiable).
[DEBUG] [2024-05-02T17:51:54Z] [000000016fe77000] [S3MetaRequest] - id=0x14d240710: Request 0x14bf45f40 finished with error code 14343 (aws-c-s3: AWS_ERROR_S3_INVALID_RESPONSE_STATUS, Invalid response status from request) and response status 416
[ERROR] [2024-05-02T17:51:54Z] [000000016fe77000] [S3MetaRequest] - id=0x14d240710 Meta request cannot recover from error 14343 (Invalid response status from request). (request=0x14bf45f40, response status=416)
[INFO] [2024-05-02T17:51:54Z] [000000016fe77000] [http-connection] - id=0x14bf893d0: Shutting down connection with error code 0 (AWS_ERROR_SUCCESS).
[DEBUG] [2024-05-02T17:51:54Z] [000000016fe77000] [task-scheduler] - id=0x14bf87948: Scheduling channel_shutdown task for immediate execution
[DEBUG] [2024-05-02T17:51:54Z] [000000016fe77000] [S3MetaRequest] - id=0x14d240710 Detected empty file with request 0x14bf45f40. Sending new request without range header.
```

And here is the one when using minio:
```
[INFO] [2024-05-02T17:55:17Z] [000000016ffcb000] [http-connection] - id=0x15268a730: HTTP/1.1 client connection established.
[DEBUG] [2024-05-02T17:55:17Z] [000000016ffcb000] [connection-manager] - id=0x14264f060: Received new connection (id=0x15268a730) from http layer
[DEBUG] [2024-05-02T17:55:17Z] [000000016ffcb000] [connection-manager] - id=0x14264f060: Grabbing pooled connectDEBUG [AwsEventLoop 3] 2024-05-02 10:55:17,197 Logger.java:85 - Received failed response: 416, Request ID: 17CBBD91D56F078B, Extended Request ID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
2024-05-02 10:55:17,197 [AwsEventLoop 3] DEBUG software.amazon.awssdk.requestId - Received failed response: 416, Request ID: 17CBBD91D56F078B, Extended Request ID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
DEBUG [AwsEventLoop 3] 2024-05-02 10:55:17,197 Logger.java:85 - Received failed response: 416, Request ID: 17CBBD91D56F078B, Extended Request ID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
2024-05-02 10:55:17,197 [AwsEventLoop 3] DEBUG software.amazon.awssdk.request - Received failed response: 416, Request ID: 17CBBD91D56F078B, Extended Request ID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
TRACE [AwsEventLoop 3] 2024-05-02 10:55:17,197 Logger.java:127 - Received request() with 9223372036854775807
```

The logs lines that indicate the differences in error response between S3 and mino are the following:
```
Request 0x14bf45f40 finished with error code 14343 (aws-c-s3: AWS_ERROR_S3_INVALID_RESPONSE_STATUS, Invalid response status from request) and response status 416
```

VS

```
2024-05-02 10:55:17,197 [AwsEventLoop 3] DEBUG software.amazon.awssdk.request - Received failed response: 416, Request ID: 17CBBD91D56F078B, Extended Request ID: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8

```
So the minio error response is lacking the `ActualObjectSize` which the CRT clients relies on. Compare this S3 trace 
S3
```
<Error><Code>InvalidRange</Code><Message>The requested range is not satisfiable</Message>
<RangeRequested>bytes=0-9</RangeRequested><ActualObjectSize>0</ActualObjectSize>
<RequestId>7QQ7F9DRRS8365A4</RequestId>
<HostId>p73Q7Ei9b5MpK0XWwooFdkgzkIkMOftkzYCG/z4PubPGszJUiok/5RcZCEBekGZVS+T3QJX+iFk=</HostId></Error>

```

with the minio trace:
```
<Error><Code>InvalidRange</Code><Message>The requested range is not satisfiable</Message><Key>my-key</Key>
<BucketName>a98c4bd72-cdf5-4273-a4ac-2177dd6eee24</BucketName><Resource>/a98c4bd72-cdf5-4273-a4ac-
2177dd6eee24/my-key</Resource><RequestId>17CB84CC9384B7B6</RequestId>
<HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>

```

And here is the low level c code used in CRT that utilizes `ActualObjectSize`: [ActualObjectSize](https://github.com/awslabs/aws-c-s3/blob/f222ada3392c94bdf77d0d889400d1128c90ee8c/source/s3_auto_ranged_get.c#L564) check and [retry](https://github.com/awslabs/aws-c-s3/blob/f222ada3392c94bdf77d0d889400d1128c90ee8c/source/s3_auto_ranged_get.c#L689) logic for reference 
Oh yeah I wondered that when I saw that response from AWS S3, they have added some new field. Silly behavior but we can comply no problem. 


It is an odd design choice of not using Stat() to verify the size of the object v/s randomly sending a range request and then retrying without it :-) 

Fix patch:
diff --git a/cmd/api-errors.go b/cmd/api-errors.go
index c4aeb9a6a99de..a35df607fadb7 100644
--- a/cmd/api-errors.go
+++ b/cmd/api-errors.go
@@ -56,19 +56,23 @@ type APIError struct {
 	Code           string
 	Description    string
 	HTTPStatusCode int
+	ObjectSize     string
+	RangeRequested string
 }
 
 // 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"`
+	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"`
+	ActualObjectSize string `xml:"ActualObjectSize,omitempty" json:"ActualObjectSize,omitempty"`
+	RangeRequested   string `xml:"RangeRequested,omitempty" json:"RangeRequested,omitempty"`
 }
 
 // APIErrorCode type of error status.
@@ -2412,10 +2416,9 @@ func toAPIError(ctx context.Context, err error) APIError {
 	apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
 	switch apiErr.Code {
 	case "NotImplemented":
-		desc := fmt.Sprintf("%s (%v)", apiErr.Description, err)
 		apiErr = APIError{
 			Code:           apiErr.Code,
-			Description:    desc,
+			Description:    fmt.Sprintf("%s (%v)", apiErr.Description, err),
 			HTTPStatusCode: apiErr.HTTPStatusCode,
 		}
 	case "XMinioBackendDown":
@@ -2432,7 +2435,19 @@ func toAPIError(ctx context.Context, err error) APIError {
 				HTTPStatusCode: e.HTTPStatusCode,
 			}
 		case batchReplicationJobError:
-			apiErr = APIError(e)
+			apiErr = APIError{
+				Description:    e.Description,
+				Code:           e.Code,
+				HTTPStatusCode: e.HTTPStatusCode,
+			}
+		case InvalidRange:
+			apiErr = APIError{
+				Code:           "InvalidRange",
+				Description:    e.Error(),
+				HTTPStatusCode: errorCodes[ErrInvalidRange].HTTPStatusCode,
+				ObjectSize:     strconv.FormatInt(e.ResourceSize, 10),
+				RangeRequested: fmt.Sprintf("%d-%d", e.OffsetBegin, e.OffsetEnd),
+			}
 		case InvalidArgument:
 			apiErr = APIError{
 				Code:           "InvalidArgument",
@@ -2559,13 +2574,15 @@ func getAPIError(code APIErrorCode) APIError {
 func getAPIErrorResponse(ctx context.Context, err APIError, resource, requestID, hostID string) APIErrorResponse {
 	reqInfo := logger.GetReqInfo(ctx)
 	return APIErrorResponse{
-		Code:       err.Code,
-		Message:    err.Description,
-		BucketName: reqInfo.BucketName,
-		Key:        reqInfo.ObjectName,
-		Resource:   resource,
-		Region:     globalSite.Region,
-		RequestID:  requestID,
-		HostID:     hostID,
+		Code:             err.Code,
+		Message:          err.Description,
+		BucketName:       reqInfo.BucketName,
+		Key:              reqInfo.ObjectName,
+		Resource:         resource,
+		Region:           globalSite.Region,
+		RequestID:        requestID,
+		HostID:           hostID,
+		ActualObjectSize: err.ObjectSize,
+		RangeRequested:   err.RangeRequested,
 	}
 }
diff --git a/cmd/batch-handlers.go b/cmd/batch-handlers.go
index bd65fa9c74b37..12bf67ac64836 100644
--- a/cmd/batch-handlers.go
+++ b/cmd/batch-handlers.go
@@ -1231,6 +1231,7 @@ type batchReplicationJobError struct {
 	Code           string
 	Description    string
 	HTTPStatusCode int
+	ObjectSize     int64
 }
 
 func (e batchReplicationJobError) Error() string {
diff --git a/cmd/erasure-object.go b/cmd/erasure-object.go
index b819d5ad35050..30181d6a4b258 100644
--- a/cmd/erasure-object.go
+++ b/cmd/erasure-object.go
@@ -251,6 +251,18 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
 		opts.NoDecryption = true
 	}
 
+	if objInfo.Size == 0 {
+		if _, _, err := rs.GetOffsetLength(objInfo.Size); err != nil {
+			// Make sure to return object info to provide extra information.
+			return &GetObjectReader{
+				ObjInfo: objInfo,
+			}, err
+		}
+
+		// Zero byte objects don't even need to further initialize pipes etc.
+		return NewGetObjectReaderFromReader(bytes.NewReader(nil), objInfo, opts)
+	}
+
 	if objInfo.IsRemote() {
 		gr, err := getTransitionedObjectReader(ctx, bucket, object, rs, h, objInfo, opts)
 		if err != nil {
@@ -260,11 +272,6 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
 		return gr.WithCleanupFuncs(nsUnlocker), nil
 	}
 
-	if objInfo.Size == 0 {
-		// Zero byte objects don't even need to further initialize pipes etc.
-		return NewGetObjectReaderFromReader(bytes.NewReader(nil), objInfo, opts)
-	}
-
 	fn, off, length, err := NewGetObjectReader(rs, objInfo, opts)
 	if err != nil {
 		return nil, err
diff --git a/cmd/httprange.go b/cmd/httprange.go
index db22299054242..2e198d47425fd 100644
--- a/cmd/httprange.go
+++ b/cmd/httprange.go
@@ -60,7 +60,11 @@ func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err er
 		}
 
 	case h.Start >= resourceSize:
-		return 0, errInvalidRange
+		return 0, InvalidRange{
+			OffsetBegin:  h.Start,
+			OffsetEnd:    h.End,
+			ResourceSize: resourceSize,
+		}
 
 	case h.End > -1:
 		end := h.End
diff --git a/cmd/object-api-errors.go b/cmd/object-api-errors.go
index 10c34f4158e56..e04cb2dd09764 100644
--- a/cmd/object-api-errors.go
+++ b/cmd/object-api-errors.go
@@ -595,7 +595,7 @@ type InvalidRange struct {
 }
 
 func (e InvalidRange) Error() string {
-	return fmt.Sprintf("The requested range \"bytes %d -> %d of %d\" is not satisfiable.", e.OffsetBegin, e.OffsetEnd, e.ResourceSize)
+	return fmt.Sprintf("The requested range 'bytes=%d-%d' is not satisfiable", e.OffsetBegin, e.OffsetEnd)
 }
 
 // ObjectTooLarge error returned when the size of the object > max object size allowed (5G) per request.
@@ -758,6 +758,9 @@ func isErrMethodNotAllowed(err error) bool {
 }
 
 func isErrInvalidRange(err error) bool {
+	if errors.Is(err, errInvalidRange) {
+		return true
+	}
 	_, ok := err.(InvalidRange)
 	return ok
 }


Test patch:
diff --git a/cmd/httprange_test.go b/cmd/httprange_test.go
index 2ce9c4e9f2497..ea13a3800ccb4 100644
--- a/cmd/httprange_test.go
+++ b/cmd/httprange_test.go
@@ -72,7 +72,7 @@ func TestHTTPRequestRangeSpec(t *testing.T) {
 		if err == nil {
 			t.Errorf("Case %d: Did not get an expected error - got %v", i, rs)
 		}
-		if err == errInvalidRange {
+		if isErrInvalidRange(err) {
 			t.Errorf("Case %d: Got invalid range error instead of a parse error", i)
 		}
 		if rs != nil {
@@ -95,7 +95,7 @@ func TestHTTPRequestRangeSpec(t *testing.T) {
 		if err1 == nil {
 			o, l, err2 = rs.GetOffsetLength(resourceSize)
 		}
-		if err1 == errInvalidRange || (err1 == nil && err2 == errInvalidRange) {
+		if isErrInvalidRange(err1) || (err1 == nil && isErrInvalidRange(err2)) {
 			continue
 		}
 		t.Errorf("Case %d: Expected errInvalidRange but: %v %v %d %d %v", i, rs, err1, o, l, err2)