Skip to content

fix: HEAD no longer downloads the full body from GCS#60

Merged
daichirata merged 1 commit into
masterfrom
fix/head-no-body
May 21, 2026
Merged

fix: HEAD no longer downloads the full body from GCS#60
daichirata merged 1 commit into
masterfrom
fix/head-no-body

Conversation

@daichirata

Copy link
Copy Markdown
Owner

Summary

Resolves #59 (reported by @MN755).

Previously, HEAD requests fell through the same code path as GET: `streamObject` / `streamRange` would open a `*storage.Reader` and call `io.Copy(w, objr)`. `net/http` does discard the body for HEAD, but the Read side of `io.Copy` still pulled every byte from GCS — so a `HEAD` on a 1 GB object racked up the same GCS read latency, egress, and billing as a `GET`.

Fix

Both `streamObject` and `streamRange` now short-circuit before opening a GCS reader when `r.Method == http.MethodHead`. Headers are derived from the object attrs we already have:

  • `Content-Encoding` / `Content-Length` are reconstructed from `attrs` + the client's `Accept-Encoding` so they match what a matching `GET` would produce.
  • The full Range parsing pipeline still runs first, so HEAD-with-Range correctly returns `206` / `416` / falls through to `200` under the same conditions as GET-with-Range.

Test plan

New focused HEAD tests sit alongside the existing GET / Range coverage:

Test Scenario Asserts
`TestProxy_HEAD_NoBody` plain HEAD 200, body length 0, `Accept-Ranges: bytes`
`TestProxy_HEAD_ContentLengthOptIn` HEAD with `-content-length` `Content-Length` set, body length 0
`TestProxy_HEAD_NotFound` HEAD on missing object 404
`TestProxy_HEAD_Range` HEAD + Range 206, `Content-Range`, body length 0
`TestProxy_HEAD_Range_Unsatisfiable` HEAD + out-of-bounds Range 416
`TestProxy_HEAD_Range_NonBytesUnitIgnored` HEAD + `items=0-10` 200, body length 0
`TestProxy_HEAD_GzippedObject` HEAD on gzip-stored, both with and without `Accept-Encoding: gzip` correct `Content-Encoding` derivation
  • `go test -race -count=1 ./...` passes
  • `go vet ./...` / `go build ./...` pass
  • CI green on this PR

Also tightens a couple of comments in `streamRange` that had grown longer than they needed to be.

🤖 Generated with Claude Code

Resolves #59 (reported by @MN755).

Previously, HEAD requests fell through the same code path as GET:
streamObject / streamRange would open a *storage.Reader and call
io.Copy(w, objr). net/http does discard the body for HEAD, but the
Read side of io.Copy still pulled every byte from GCS, so a HEAD on
a 1 GB object racked up the same GCS read latency, egress, and
billing as a GET.

Both streamObject and streamRange now short-circuit before opening a
GCS reader when r.Method == http.MethodHead. Headers are derived
from the object attrs we already have (Content-Encoding / -Length
are reconstructed from attrs + Accept-Encoding so they match what a
matching GET would produce). The full Range parsing pipeline still
runs first, so HEAD with Range correctly returns 206 / 416 / falls
through to 200 in the same conditions as GET.

Adds focused HEAD tests next to the existing GET/Range coverage so
this can't regress: plain HEAD, HEAD with -content-length, 404,
HEAD + Range (206 + Content-Range only), unsatisfiable range,
non-bytes unit ignored, and gzip-stored with/without Accept-Encoding:
gzip.

Also tightens a couple of comments in streamRange that had grown
longer than they needed to be.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@daichirata daichirata merged commit a5342dc into master May 21, 2026
1 check passed
@daichirata daichirata deleted the fix/head-no-body branch May 21, 2026 13:49
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.

HEAD requests still stream the full object from GCS

1 participant