mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
A HEAD on a cache hit opened the stored object and closed it unread. On S3 and Azure that is a GET whose body is thrown away, which can also drop the pooled connection; on GCS it is a GET as well. The server now marks HEAD requests in the request context, and readStored answers a marked cache hit from the database record. Size, content type and ETag already came from the record. Accept-Ranges depended on the opened reader being seekable, so backends now say whether their readers are (Blob does, GCS does not) and the HEAD response matches the GET. A HEAD still counts as a cache hit, as before. DirectServe redirects are unchanged since they are decided before the object would be opened.
88 lines
3 KiB
Go
88 lines
3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/git-pkgs/purl"
|
|
)
|
|
|
|
// A HEAD on a cache hit is answered from the artifact's database record. It
|
|
// must not open the stored object (a GET on S3 or GCS), and its headers must
|
|
// be the ones the GET sends.
|
|
func TestHeadCacheHitSkipsStorageOpen(t *testing.T) {
|
|
const digest = "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd"
|
|
for _, seekable := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("seekable=%t", seekable), func(t *testing.T) {
|
|
proxy, db, store, fetcher := setupTestProxy(t)
|
|
store.seekable = seekable
|
|
seedPackage(t, db, store, "oci", "library/nginx", digest, digest, "cached blob")
|
|
h := &ContainerHandler{
|
|
proxy: proxy,
|
|
registryURL: "https://registry.invalid",
|
|
proxyURL: "http://localhost:8080",
|
|
}
|
|
target := "/library/nginx/blobs/" + digest
|
|
|
|
get := httptest.NewRecorder()
|
|
h.Routes().ServeHTTP(get, httptest.NewRequest(http.MethodGet, target, nil))
|
|
if get.Code != http.StatusOK || get.Body.String() != "cached blob" {
|
|
t.Fatalf("GET: status %d, body %q", get.Code, get.Body.String())
|
|
}
|
|
opensAfterGet := store.opens
|
|
|
|
req := httptest.NewRequest(http.MethodHead, target, nil)
|
|
req = req.WithContext(ServingHead(req.Context()))
|
|
head := httptest.NewRecorder()
|
|
h.Routes().ServeHTTP(head, req)
|
|
|
|
if head.Code != http.StatusOK {
|
|
t.Fatalf("HEAD: status %d, body %q", head.Code, head.Body.String())
|
|
}
|
|
if got := store.opens - opensAfterGet; got != 0 {
|
|
t.Errorf("HEAD opened the stored object %d times, want 0", got)
|
|
}
|
|
if head.Body.Len() != 0 {
|
|
t.Errorf("HEAD body length = %d, want 0", head.Body.Len())
|
|
}
|
|
if !maps.EqualFunc(get.Header(), head.Header(), func(a, b []string) bool {
|
|
return fmt.Sprint(a) == fmt.Sprint(b)
|
|
}) {
|
|
t.Errorf("HEAD headers differ from GET:\n GET %v\n HEAD %v", get.Header(), head.Header())
|
|
}
|
|
if got := get.Header().Get("Accept-Ranges") == "bytes"; got != seekable {
|
|
t.Errorf("GET Accept-Ranges set = %t, want %t", got, seekable)
|
|
}
|
|
if fetcher.fetchCalled {
|
|
t.Error("fetcher called on a cache hit")
|
|
}
|
|
|
|
// A HEAD still counts as a hit, as it did when it opened the object.
|
|
art, err := db.GetArtifact(purl.MakePURLString("oci", "library/nginx", digest), digest)
|
|
if err != nil || art == nil {
|
|
t.Fatalf("artifact record: %v", err)
|
|
}
|
|
if art.HitCount != 2 {
|
|
t.Errorf("hit count = %d, want 2 (GET and HEAD)", art.HitCount)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A request that is not marked as HEAD reads the object as before.
|
|
func TestCacheHitWithoutHeadMarkOpensObject(t *testing.T) {
|
|
proxy, db, store, _ := setupTestProxy(t)
|
|
seedPackage(t, db, store, "npm", "demo", "1.0.0", "demo.tgz", "content")
|
|
result, err := proxy.GetCachedArtifact(context.Background(), "npm", "demo", "1.0.0", "demo.tgz")
|
|
if err != nil || result == nil || result.Reader == nil {
|
|
t.Fatalf("result=%+v err=%v; want an open reader", result, err)
|
|
}
|
|
_ = result.Reader.Close()
|
|
if store.opens != 1 {
|
|
t.Errorf("opens = %d, want 1", store.opens)
|
|
}
|
|
}
|