Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Answer byte ranges on raw downloads

26c9be338872126250f581f47c16ae5619f2cd2c · Keenan Nemetz · 9d ago · parent 1dfa8cbd

Answer byte ranges on raw downloads

`/r/<name>/raw/<ref>/<path>` streamed the whole blob and ignored `Range`,
so a client resuming a download or asking for a few KiB received the
whole file. The artifact route already answers `206`; this does the same
for repository content.

A blob is streamed from `git cat-file`, which cannot seek, so a range
reads and discards the bytes before it and copies only the requested
part. The win is what does not cross the network: a 64 KiB range of an
8 MiB file is still 8 MiB of reading on the server, but 64 KiB of
transfer instead of 8 MiB.

- A single `Range` is answered with `206`, `Content-Range`, and only the
  requested bytes; prefix, open-ended and suffix forms all work.
- A range that starts past the end is `416` with `Content-Range: bytes */<size>`.
- Multiple ranges, an unknown unit, and a malformed header get the whole
  file with `200`, which RFC 9110 allows.
- The no-range response keeps its bytes and gains `Accept-Ranges: bytes`.
- `HEAD` stops after the headers instead of reading the blob.

The load harness's `raw-range` scenario now reports `206`: 365.6 req/s at
concurrency 4, p50 11 ms, against 265.6 req/s and p50 15 ms when the full
file was sent.

server/web/codebrowser.go +33−2

@@ -224,13 +224,44 @@ func (w *Web) blob(rw http.ResponseWriter, r *http.Request, raw bool) {
224 224 if raw {
225 225 // The download is the one place that should carry every byte, so it is
226 226 // streamed rather than read and written: a 100 MiB asset costs one copy
227 // buffer per request, not 100 MiB of heap.
227 // buffer per request, not 100 MiB of heap. A byte range skips the bytes
228 // before it in the stream (git cannot seek) and copies only the requested
229 // part, so the win is what does not cross the network.
228 230 h := rw.Header()
229 231 h.Set("Content-Type", "text/plain; charset=utf-8")
230 h.Set("Content-Length", strconv.FormatInt(stream.Size, 10))
231 232 // The announced length is part of the contract, so a text blob must not be
232 233 // re-encoded on the way out; a compressed body has no such length.
233 234 h.Set("Cache-Control", "no-transform")
235 h.Set("Accept-Ranges", "bytes")
236 br, partial, unsatisfiable := parseByteRange(r.Header.Get("Range"), stream.Size)
237 if unsatisfiable {
238 h.Set("Content-Range", fmt.Sprintf("bytes */%d", stream.Size))
239 rw.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
240 return
241 }
242 if partial {
243 h.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", br.start, br.start+br.length-1, stream.Size))
244 h.Set("Content-Length", strconv.FormatInt(br.length, 10))
245 rw.WriteHeader(http.StatusPartialContent)
246 if r.Method == http.MethodHead {
247 return
248 }
249 if br.start > 0 {
250 if _, err := io.CopyN(io.Discard, stream, br.start); err != nil {
251 w.Logger("raw %s %s: skip: %v", m.Name, p, err)
252 return
253 }
254 }
255 if _, err := io.CopyN(rw, stream, br.length); err != nil {
256 // The client hung up mid-download; headers are already out.
257 w.Logger("raw %s %s: %v", m.Name, p, err)
258 }
259 return
260 }
261 h.Set("Content-Length", strconv.FormatInt(stream.Size, 10))
262 if r.Method == http.MethodHead {
263 return
264 }
234 265 if _, err := io.Copy(rw, stream); err != nil {
235 266 // The client hung up mid-download; headers are already out.
236 267 w.Logger("raw %s %s: %v", m.Name, p, err)