Skip to content

Serve a Range request for a cached object as 206 instead of the whole object [patch] - #107

Merged
matt-edmondson merged 3 commits into
mainfrom
fix/62-range-on-cache-hit
Oct 9, 2026
Merged

matt-edmondson merged 3 commits into
mainfrom
fix/62-range-on-cache-hit

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #62

What was wrong

ObjectRouteHandler.ServeFromStoreAsync always set the full Content-Length and streamed the whole file with 200. On a cache hit it sent no Accept-Ranges or Content-Range, and never answered 206 or 416. When git-lfs resumed an interrupted download with Range: bytes=N- and the object was already cached, it got every byte again and started over. The design spec calls for range support on a store hit. Only the miss path honoured Range.

Change

  • Hits, both direct and follower-after-leader, are now served with Results.Stream(cached, OctetStream, enableRangeProcessing: true). ASP.NET Core's range processing answers a satisfiable range with 206 and Content-Range, an unsatisfiable one with 416, and sends Accept-Ranges: bytes. This follows the triage note to use the built-in processing instead of writing a parser.
  • A request with no Range still gets 200, the whole object, and application/octet-stream.
  • The miss path is unchanged: a ranged miss is still forwarded and not stored.

Tests

New ProxyFlowTests cases each warm the store through a real download first, then hit it:

  • Download_RangeRequestOnAHit_ReturnsPartialContentFromTheStore covers a closed range (10-19), a suffix range (-5) and an open range (30-). Each checks for 206, the exact bytes, the Content-Range, and that upstream was not fetched again.
  • Download_UnsatisfiableRangeOnAHit_Returns416 checks for 416 with bytes */<length>.
  • Download_HitWithoutARange_ReturnsTheWholeObjectAndAdvertisesRanges checks for 200, the full body, Content-Length, the content type and Accept-Ranges: bytes.

With the fix reverted, all 5 new cases fail. The full suite passes locally: 359 tests, 0 failed. The branch merges cleanly with the open #101, #102, #103 and #104.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WY7QGzbH6kE2oceF1hP8fh


Generated by Claude Code

… object [patch]

ServeFromStoreAsync always set the full Content-Length and streamed the
entire file with 200, so a git-lfs download resumed with Range: bytes=N-
against a cached object started over from byte zero. The design spec calls
for range support on a store hit; only the miss path honoured Range.

Hits are now served through ASP.NET Core's range processing, which answers
a satisfiable range with 206 and Content-Range, an unsatisfiable one with
416, and advertises Accept-Ranges: bytes. A request without Range still
gets 200 and the whole object.

Fixes #62

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WY7QGzbH6kE2oceF1hP8fh
claude added 2 commits October 9, 2026 08:20
…-hit

# Conflicts:
#	GitLfsCache/Endpoints/ObjectRouteHandler.cs
…agree

The re-queue loop brought in from main tested ticket.IsLeader in a for
loop whose incrementer advanced attempt, which SonarCloud reports as a
critical S1994 finding and which failed this PR's quality gate once the
merge put the loop in its diff. The same loop as a while with the counter
advanced in the body behaves identically.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WY7QGzbH6kE2oceF1hP8fh
@sonarqubecloud

sonarqubecloud Bot commented Oct 9, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 11fadf6 into main Oct 9, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the fix/62-range-on-cache-hit branch October 9, 2026 10:22
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.

A Range request for a cached object returns the whole object with 200, so resumed git-lfs downloads start over

2 participants