@@ -320,7 +320,18 @@ func (s *Strategy) handleSnapshotRequest(w http.ResponseWriter, r *http.Request,
320320 }
321321 s .maybeBackgroundFetch (repo )
322322
323- reader , headers , err := s .cache .Open (ctx , cacheKey )
323+ // Forward only Range/If-Range here; If-Match/If-None-Match are evaluated
324+ // against the served body below via CheckConditionals.
325+ reader , headers , err := s .cache .Open (ctx , cacheKey , httputil .RangeOptions (r )... )
326+ if errors .Is (err , cache .ErrRangeNotSatisfiable ) {
327+ w .Header ().Set ("Content-Type" , "application/zstd" )
328+ w .Header ().Set ("Accept-Ranges" , "bytes" )
329+ if cr := headers .Get ("Content-Range" ); cr != "" {
330+ w .Header ().Set ("Content-Range" , cr )
331+ }
332+ w .WriteHeader (http .StatusRequestedRangeNotSatisfiable )
333+ return
334+ }
324335 if err != nil && ! errors .Is (err , os .ErrNotExist ) {
325336 logger .ErrorContext (ctx , "Failed to open snapshot from cache" , "upstream" , upstreamURL , "error" , err )
326337 http .Error (w , "Internal server error" , http .StatusInternalServerError )
@@ -517,23 +528,29 @@ func (s *Strategy) handleBundleRequest(w http.ResponseWriter, r *http.Request, h
517528}
518529
519530func (s * Strategy ) serveSnapshotWithBundle (ctx context.Context , w http.ResponseWriter , r * http.Request , reader io.ReadCloser , headers http.Header , repo * gitclone.Repository , upstreamURL , repoName string , start time.Time ) error {
520- snapshotCommit := headers .Get ("X-Cachew-Snapshot-Commit" )
521- mirrorHead := s .getMirrorHead (ctx , repo )
522-
523- // Forward the snapshot commit to the client so it knows whether the
524- // snapshot is fresh (no bundle URL = already at HEAD, skip freshen).
525- if snapshotCommit != "" {
526- w .Header ().Set ("X-Cachew-Snapshot-Commit" , snapshotCommit )
531+ // A satisfied byte range is served as-is. Bundle negotiation and full-body
532+ // conditionals apply only to whole-snapshot downloads, so a partial read
533+ // (e.g. a client.ParallelGet chunk) skips them and returns 206 directly.
534+ if cr := headers .Get ("Content-Range" ); cr != "" {
535+ applySnapshotCacheHeaders (w , headers )
536+ w .Header ().Set ("Accept-Ranges" , "bytes" )
537+ w .Header ().Set ("Content-Range" , cr )
538+ // Ranged clients (client.ParallelGet) read the freshen metadata from the
539+ // discovery chunk, so it must be present on partial responses too.
540+ s .setSnapshotMetadataHeaders (ctx , w , headers , repo , upstreamURL )
541+ w .WriteHeader (http .StatusPartialContent )
542+ n , err := io .Copy (w , reader )
543+ s .metrics .recordSnapshotServe (ctx , "cache_range" , repoName , n , time .Since (start ))
544+ if span := trace .SpanFromContext (ctx ); span .SpanContext ().IsValid () {
545+ span .SetAttributes (attribute .String ("cachew.source" , "cache_range" ), attribute .Int64 ("cachew.bytes" , n ))
546+ }
547+ return errors .Wrap (err , "stream snapshot range" )
527548 }
528549
529- if snapshotCommit != "" && mirrorHead != "" && snapshotCommit != mirrorHead {
530- repoPath , err := gitclone .RepoPathFromURL (upstreamURL )
531- if err == nil {
532- bundleURL := fmt .Sprintf ("/git/%s/snapshot.bundle?base=%s" , repoPath , snapshotCommit )
533- w .Header ().Set ("X-Cachew-Bundle-Url" , bundleURL )
534- }
550+ snapshotCommit , bundleURL := s .setSnapshotMetadataHeaders (ctx , w , headers , repo , upstreamURL )
535551
536- // Proactively generate and cache the bundle so any pod can serve it.
552+ // Proactively generate and cache the advertised bundle so any pod can serve it.
553+ if bundleURL != "" {
537554 go func () {
538555 bgCtx := context .WithoutCancel (ctx )
539556 logger := logging .FromContext (bgCtx )
@@ -550,6 +567,7 @@ func (s *Strategy) serveSnapshotWithBundle(ctx context.Context, w http.ResponseW
550567 }
551568
552569 applySnapshotCacheHeaders (w , headers )
570+ w .Header ().Set ("Accept-Ranges" , "bytes" )
553571
554572 // Honour conditional GETs against the advertised ETag. ServeContent does this
555573 // natively for *os.File readers, but cache backends returning non-file readers
@@ -569,6 +587,33 @@ func (s *Strategy) serveSnapshotWithBundle(ctx context.Context, w http.ResponseW
569587 return errors .Wrap (err , "stream snapshot" )
570588}
571589
590+ // setSnapshotMetadataHeaders advertises the snapshot's commit and, when the
591+ // snapshot trails the mirror's HEAD, the delta-bundle URL clients use to
592+ // fast-forward. Shared by the full and ranged serve paths so ranged clients
593+ // (client.ParallelGet) receive the same freshen metadata on the discovery
594+ // chunk. It returns the snapshot commit and the bundle URL it set (empty when
595+ // the snapshot is already at HEAD), so callers can decide whether to
596+ // pre-generate the bundle.
597+ func (s * Strategy ) setSnapshotMetadataHeaders (ctx context.Context , w http.ResponseWriter , headers http.Header , repo * gitclone.Repository , upstreamURL string ) (snapshotCommit , bundleURL string ) {
598+ snapshotCommit = headers .Get ("X-Cachew-Snapshot-Commit" )
599+ if snapshotCommit == "" {
600+ return "" , ""
601+ }
602+ w .Header ().Set ("X-Cachew-Snapshot-Commit" , snapshotCommit )
603+
604+ mirrorHead := s .getMirrorHead (ctx , repo )
605+ if mirrorHead == "" || snapshotCommit == mirrorHead {
606+ return snapshotCommit , ""
607+ }
608+ repoPath , err := gitclone .RepoPathFromURL (upstreamURL )
609+ if err != nil {
610+ return snapshotCommit , ""
611+ }
612+ bundleURL = fmt .Sprintf ("/git/%s/snapshot.bundle?base=%s" , repoPath , snapshotCommit )
613+ w .Header ().Set ("X-Cachew-Bundle-Url" , bundleURL )
614+ return snapshotCommit , bundleURL
615+ }
616+
572617// applySnapshotCacheHeaders forwards the cached snapshot's validators so clients
573618// can revalidate (ETag) and size the transfer (Content-Length). Content-Type is
574619// fixed for snapshots regardless of what the cache backend recorded.
0 commit comments