Skip to content

fix: getObject with range request and then stat would not return the correct range size - #2267

Merged
harshavardhana merged 14 commits into
minio:masterfrom
jiuker:fix-getObject-with-range-request-and-then-stat-would-return-the-filesize
Jul 30, 2026
Merged

harshavardhana merged 14 commits into
minio:masterfrom
jiuker:fix-getObject-with-range-request-and-then-stat-would-return-the-filesize

Conversation

@jiuker

@jiuker jiuker commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

fix: getObject with range request and then stat would return the filesize

fix #1813

context:

basically Stat() should be fixed to report the current io.Reader contract not show the original file size
Stat() without range is needed to calculate the Seek/ReadAt margin
but once that is retrieved the Stat() call must return the originalSize - range prroperly
not the originalSize

testGetObjectWithRange() Test Summary

Action Legend

Action Description
New() Create a new GetObject reader with the current Range config
Size(n) Call Stat() and verify the remaining readable size equals n
Read(n) Read n bytes using Read() — advances the read offset
ReadFull(n) Read n bytes using Read() (same as Read, just named differently in test)
ReadAt(offset, n) Read n bytes at the given offset using ReadAt() — does NOT change the read offset
Seek(offset, whence) Seek to a new position using Seek() — changes the read offset
ReadAll() Read all remaining bytes using io.ReadAll() — consumes everything to EOF

Range Settings

testIndex Range Config Description baseSize
0 No Range (full) Read the entire object (33KB) 33KB
1 SetRange(100, 1000) Read bytes 100~1000 (inclusive) 901
2 SetRange(100, 0) Read from byte 100 to end 33KB - 100
3 SetRange(0, 1000) Read from byte 0 to 1000 1001

Operation Combinations (executed for each Range)

Case Operation Sequence Test Focus
1 New → Size → Read×3 → Size → ReadFull → Size Stat first then sequential Read, verify Size decrements correctly
2 New → Read×3 → Size → ReadFull → Size Read first then Stat, verify offset accumulation
3 New → Size → Read → Size → ReadAt → Size → Read → Size After Stat+Read, execute ReadAt — verify ReadAt does NOT move the offset
4 New → Read → Size → ReadAt → Size → Read → Size After Read, execute ReadAt — verify ReadAt does not affect offset
5 New → Size → Read → Size → ReadAt → Size → Read → Size → Seek → Size → Read → Size Extends case 3 with Seek(SeekCurrent), verify offset after Seek
6 New → Read → Size → ReadAt → Size → Read → Size → Seek → Size → Read → Size Extends case 4 with Seek, verify combined operations
7 New → Size → ReadAll → ReadAt Stat then ReadAll to consume entire content, then ReadAt
8 New → ReadAll → ReadAt Directly ReadAll to consume entire content, then ReadAt
9 New → Seek → Size → Read → Size Directly Seek to set the cursor, then check the size.

Total: 4 Range modes × 8 operation combinations = 32 sub-test scenarios, covering all common read operation patterns and offset management correctness for ranged GetObject.

Summary by CodeRabbit

Summary of changes

  • Bug Fixes
    • Improved object read/seek handling by preserving the caller’s byte-range across sequential operations.
    • Ensured object-info–only requests no longer interfere with range headers and size reporting.
    • Fixed Stat() to correctly report remaining size and detect end-of-object (EOF) after seek/read sequences.

…size

fix: getObject with range request and then stat would return the filesize
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

GetObject preserves caller-provided ranges across initial Stat, Seek, and metadata requests, resets stale readers for ReadAt, and tracks total object size so Stat() reports remaining range size and EOF correctly.

Changes

GetObject range and size handling

Layer / File(s) Summary
Range-aware request flow
api-get-object.go
Preserves or removes the original Range header conditionally, closes the initial reader after ReadAt, and re-fetches when the HTTP reader is nil.
Object size propagation and Stat results
api-get-object.go
Propagates ObjectSize into Object.totalSize and computes remaining size from currOffset, returning io.EOF at the object end.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant GetObject
  participant HTTPReader
  participant Object
  Caller->>GetObject: Create object with Range
  GetObject->>HTTPReader: Fetch metadata or object data
  HTTPReader-->>GetObject: Return response and object size
  GetObject->>Object: Store totalSize
  Caller->>Object: Call Stat
  Object-->>Caller: Return remaining size or io.EOF
Loading

Possibly related PRs

Suggested reviewers: harshavardhana

Poem

A rabbit kept the Range in sight,
And measured bytes from left to right.
Stat marked the end,
ReadAt found a friend,
And EOF arrived just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #1813 by keeping the range for pre-read Stat calls and reporting the ranged size.
Out of Scope Changes check ✅ Passed The added size-tracking and header-handling changes support the reported bug and stay within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: Stat after a ranged GetObject now reports the requested range size.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jiuker

This comment was marked as off-topic.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api-get-object.go`:
- Around line 516-517: Remove the unused disableRange: true assignment in
api-get-object.go lines 516-517. Update the stale comment at lines 152-158 to
refer only to the initial Seek/StatObject behavior, and correct the disableRange
field comment at line 284 to describe disabling the range header for Seek
requests and fix its typo.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 969cbd06-bd2c-4298-99ce-89e606b0c3bc

📥 Commits

Reviewing files that changed from the base of the PR and between ab51b38 and ee95e0d.

📒 Files selected for processing (1)
  • api-get-object.go

Comment thread api-get-object.go Outdated
Comment thread api-get-object.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api-get-object.go`:
- Around line 470-475: Update Stat() to create and return a copy of ObjectInfo
with Size adjusted to the remaining bytes, rather than mutating
o.objectInfo.Size. Preserve the canonical size stored in o.objectInfo so Seek()
continues using the original object boundary, including the related logic at the
additional Stat() location.
- Around line 159-167: Update the first-read/refetch flow around the Range
handling in GetObject so originalRangeHeader is restored before fetching after
Stat, including the paths corresponding to the noted additional locations. Do
not unconditionally delete the caller’s Range for offset-zero reads; preserve it
unless the caller explicitly supplies a different range or offset, while
retaining the existing behavior for stat operations that require the header
removed.
- Line 206: Update the response construction paths in the object GET handler,
including normal reads, metadata, empty-object handling, and RDMA, to always
populate object size and track whether it is present separately from its numeric
value. Revise Stat() to use the explicit size-present indicator rather than
checking totalSize != 0, preserving valid zero-byte object responses without
returning io.EOF.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9f6e0457-2527-4be0-b3d7-cd93423a7fa6

📥 Commits

Reviewing files that changed from the base of the PR and between d723b0f and 1c642df.

📒 Files selected for processing (1)
  • api-get-object.go

Comment thread api-get-object.go
Comment thread api-get-object.go
Comment thread api-get-object.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
api-get-object.go (2)

222-242: ⚠️ Potential issue | 🔴 Critical

Restore the caller’s range on the first refetch after Stat().

Line 222 now re-enters this path when httpReader == nil, but the offset-zero branch at Lines 239-242 still deletes the caller’s Range. Therefore GetObject(..., Range: bytes=100-1000); Stat(); Read() can fetch the full object instead of the requested range. Restore originalRangeHeader before this GET unless a later operation intentionally replaces it. (raw.githubusercontent.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-get-object.go` around lines 222 - 242, Update the refetch logic in the
request path guarded by req.DidOffsetChange, !req.beenRead, or httpReader == nil
so the offset-zero branch does not delete the caller’s original Range header
after Stat(). Restore originalRangeHeader before issuing the GET, while
preserving intentional range replacements from req.isReadAt and nonzero
req.Offset.

307-307: ⚠️ Potential issue | 🔴 Critical

Populate totalSize on every applicable object path.

doGetRequest only updates totalSize when ObjectSize != 0, while the non-first read response at Lines 278-283 omits ObjectSize and the RDMA constructor does not initialize totalSize. A ReadAt-first-then-Read sequence can therefore leave totalSize == 0, causing a later Stat() to return io.EOF for a non-empty object. Initialize RDMA totalSize and propagate object size for full-range reads; use an explicit presence indicator if zero-byte objects must be distinguished from “unset.” (raw.githubusercontent.com)

Also applies to: 326-326, 384-387

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-get-object.go` at line 307, Update doGetRequest and the RDMA object
constructor so totalSize is populated on every applicable read path, including
non-first/full-range responses and ReadAt followed by Read. Initialize the RDMA
totalSize from the object metadata and propagate ObjectSize where currently
omitted; use an explicit presence indicator if ObjectSize == 0 must remain
distinguishable from an unset value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api-get-object.go`:
- Around line 476-482: Update Stat() to copy o.objectInfo into a local
ObjectInfo value before adjusting Size; return that copy for both the currOffset
> totalSize EOF path and the normal path. Preserve the canonical
o.objectInfo.Size unchanged so Seek() and setOffset() continue using the full
object boundary.

---

Outside diff comments:
In `@api-get-object.go`:
- Around line 222-242: Update the refetch logic in the request path guarded by
req.DidOffsetChange, !req.beenRead, or httpReader == nil so the offset-zero
branch does not delete the caller’s original Range header after Stat(). Restore
originalRangeHeader before issuing the GET, while preserving intentional range
replacements from req.isReadAt and nonzero req.Offset.
- Line 307: Update doGetRequest and the RDMA object constructor so totalSize is
populated on every applicable read path, including non-first/full-range
responses and ReadAt followed by Read. Initialize the RDMA totalSize from the
object metadata and propagate ObjectSize where currently omitted; use an
explicit presence indicator if ObjectSize == 0 must remain distinguishable from
an unset value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5ced866e-6747-4c0e-9baf-f2eb4c232b70

📥 Commits

Reviewing files that changed from the base of the PR and between 1c642df and cd212e6.

📒 Files selected for processing (1)
  • api-get-object.go

Comment thread api-get-object.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api-get-object.go (1)

152-159: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Do not use zero as the “ObjectSize unavailable” sentinel.

ObjectSize == 0 is valid for an empty object, but doGetRequest only records non-zero values. The same value is also emitted for first ReadAt responses where the total size is unknown, so Stat() cannot distinguish an empty object from missing metadata and may report incorrect EOF/size results. Add an explicit size-present flag.

Also applies to: 282-289, 313-313, 332-332, 390-393

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-get-object.go` around lines 152 - 159, Replace the zero-value ObjectSize
sentinel with an explicit size-present flag across doGetRequest and its
getResponse handling. Mark the size as present when metadata reports an object,
including valid zero-length objects, while leaving first ReadAt responses
unknown when total size is unavailable. Propagate the flag through all response
construction and update Stat() to distinguish absent metadata from a known empty
object.
♻️ Duplicate comments (2)
api-get-object.go (2)

166-174: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the caller’s Range before refetching after Stat.

The Stat path preserves originalRangeHeader, but the subsequent offset-zero refetch still unconditionally deletes opts.headers["Range"]. Therefore GetObject(..., Range: bytes=100-1000); Stat(); Read() can fetch the full object instead of the requested range. Restore the original range in this branch unless the caller explicitly changed the range or offset.

Proposed fix
 					} else {
-						delete(opts.headers, "Range")
+						if originalRangeHeader != "" {
+							opts.headers["Range"] = originalRangeHeader
+						} else {
+							delete(opts.headers, "Range")
+						}
 					}

Also applies to: 241-245

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-get-object.go` around lines 166 - 174, Update the offset-zero refetch
logic in the GetObject flow to restore originalRangeHeader after Stat instead of
unconditionally deleting opts.headers["Range"]. Preserve an explicitly changed
caller range or offset, and only remove the Range header when the original
request had none.

482-487: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Return a copy from Stat() instead of mutating o.objectInfo.

Seek() and related offset checks use the canonical o.objectInfo.Size. After Stat() reduces that field to the remaining bytes, later seeks and reads can incorrectly stop at the reduced boundary. Copy o.objectInfo, adjust the copy’s Size, and return the copy in both branches.

Also applies to: 507-512

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api-get-object.go` around lines 482 - 487, Update Stat() to preserve the
canonical o.objectInfo by copying it into a local ObjectInfo value before
adjusting Size. In both the o.currOffset > o.totalSize and o.currOffset <=
o.totalSize branches, modify and return the copy rather than mutating or
returning o.objectInfo, so Seek() and subsequent reads continue using the
original size.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@api-get-object.go`:
- Around line 152-159: Replace the zero-value ObjectSize sentinel with an
explicit size-present flag across doGetRequest and its getResponse handling.
Mark the size as present when metadata reports an object, including valid
zero-length objects, while leaving first ReadAt responses unknown when total
size is unavailable. Propagate the flag through all response construction and
update Stat() to distinguish absent metadata from a known empty object.

---

Duplicate comments:
In `@api-get-object.go`:
- Around line 166-174: Update the offset-zero refetch logic in the GetObject
flow to restore originalRangeHeader after Stat instead of unconditionally
deleting opts.headers["Range"]. Preserve an explicitly changed caller range or
offset, and only remove the Range header when the original request had none.
- Around line 482-487: Update Stat() to preserve the canonical o.objectInfo by
copying it into a local ObjectInfo value before adjusting Size. In both the
o.currOffset > o.totalSize and o.currOffset <= o.totalSize branches, modify and
return the copy rather than mutating or returning o.objectInfo, so Seek() and
subsequent reads continue using the original size.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f91a0a9a-97fd-4a20-abda-537630e7941b

📥 Commits

Reviewing files that changed from the base of the PR and between cd212e6 and 4e5705d.

📒 Files selected for processing (1)
  • api-get-object.go

@jiuker
jiuker marked this pull request as draft July 22, 2026 03:54
@jiuker
jiuker marked this pull request as ready for review July 22, 2026 09:33
@jiuker jiuker changed the title fix: getObject with range request and then stat would return the filesize fix: getObject with range request and then stat would not return the correct rang size Jul 23, 2026
@jiuker jiuker changed the title fix: getObject with range request and then stat would not return the correct rang size fix: getObject with range request and then stat would not return the correct range size Jul 23, 2026
@klauspost

Copy link
Copy Markdown
Contributor

OK, looks good. Let's update the docs at

minio-go/api-get-object.go

Lines 460 to 461 in d92b1d2

// Stat returns the ObjectInfo structure describing Object.
func (o *Object) Stat() (ObjectInfo, error) {

If this is correct, something like this:

// Stat returns the ObjectInfo structure describing Object.
// When requesting a partial object or reading has started,
// the size returned will reflect the remaining size.
func (o *Object) Stat() (ObjectInfo, error) {

@jiuker
jiuker requested a review from klauspost July 24, 2026 01:14
@jiuker
jiuker force-pushed the fix-getObject-with-range-request-and-then-stat-would-return-the-filesize branch from f5cecaf to 5018182 Compare July 24, 2026 02:41
@harshavardhana
harshavardhana merged commit 802bd60 into minio:master Jul 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Running Stat on GetObject before reading discards range

3 participants