diff --git a/client/client.go b/client/client.go index 34e8bdc7..6b2e1880 100644 --- a/client/client.go +++ b/client/client.go @@ -30,7 +30,6 @@ func (e *HTTPStatusError) Error() string { // transportHeaders are headers added by the HTTP transport layer that should // not be surfaced as cached-object metadata on responses. var transportHeaders = []string{ //nolint:gochecknoglobals - "Content-Length", "Date", "Accept-Encoding", "User-Agent", diff --git a/internal/cache/cachetest/suite.go b/internal/cache/cachetest/suite.go index afa484fe..7790e6e7 100644 --- a/internal/cache/cachetest/suite.go +++ b/internal/cache/cachetest/suite.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "os" + "strconv" "testing" "time" @@ -57,6 +58,10 @@ func Suite(t *testing.T, newCache func(t *testing.T) cache.Cache) { testLastModified(t, newCache(t)) }) + t.Run("ContentLength", func(t *testing.T) { + testContentLength(t, newCache(t)) + }) + t.Run("NamespaceIsolation", func(t *testing.T) { testNamespaceIsolation(t, newCache(t)) }) @@ -343,6 +348,29 @@ func testLastModified(t *testing.T, c cache.Cache) { assert.Equal(t, explicitTime.Format(http.TimeFormat), headers2.Get("Last-Modified")) } +func testContentLength(t *testing.T, c cache.Cache) { + defer c.Close() + ctx := t.Context() + + content := []byte("hello content length") + key := cache.NewKey("test-content-length") + + w, err := c.Create(ctx, key, nil, time.Hour) + assert.NoError(t, err) + _, err = w.Write(content) + assert.NoError(t, err) + assert.NoError(t, w.Close()) + + reader, headers, err := c.Open(ctx, key) + assert.NoError(t, err) + defer reader.Close() + assert.Equal(t, strconv.Itoa(len(content)), headers.Get("Content-Length")) + + statHeaders, err := c.Stat(ctx, key) + assert.NoError(t, err) + assert.Equal(t, strconv.Itoa(len(content)), statHeaders.Get("Content-Length")) +} + func testNamespaceIsolation(t *testing.T, c cache.Cache) { defer c.Close() ctx := t.Context() diff --git a/internal/cache/disk.go b/internal/cache/disk.go index 9fbd0903..6adc4ade 100644 --- a/internal/cache/disk.go +++ b/internal/cache/disk.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "sync/atomic" "time" @@ -234,7 +235,8 @@ func (d *Disk) Stat(ctx context.Context, key Key) (http.Header, error) { path := d.keyToPath(d.namespace, key) fullPath := filepath.Join(d.config.Root, path) - if _, err := os.Stat(fullPath); err != nil { + info, err := os.Stat(fullPath) + if err != nil { return nil, errors.Errorf("failed to stat file: %w", err) } @@ -252,6 +254,7 @@ func (d *Disk) Stat(ctx context.Context, key Key) (http.Header, error) { return nil, errors.Errorf("failed to get headers: %w", err) } + headers.Set("Content-Length", strconv.FormatInt(info.Size(), 10)) return headers, nil } @@ -279,6 +282,12 @@ func (d *Disk) Open(ctx context.Context, key Key) (io.ReadCloser, http.Header, e return nil, nil, errors.Join(errors.Errorf("failed to get headers: %w", err), f.Close()) } + finfo, err := f.Stat() + if err != nil { + return nil, nil, errors.Join(errors.Errorf("failed to stat file for size: %w", err), f.Close()) + } + headers.Set("Content-Length", strconv.FormatInt(finfo.Size(), 10)) + // Reset expiration time to implement LRU ttl := min(expiresAt.Sub(now), d.config.MaxTTL) newExpiresAt := now.Add(ttl) diff --git a/internal/cache/memory.go b/internal/cache/memory.go index d82944b4..31c73345 100644 --- a/internal/cache/memory.go +++ b/internal/cache/memory.go @@ -8,6 +8,7 @@ import ( "maps" "net/http" "os" + "strconv" "sync" "sync/atomic" "time" @@ -75,7 +76,9 @@ func (m *Memory) Stat(_ context.Context, key Key) (http.Header, error) { return nil, os.ErrNotExist } - return entry.headers, nil + headers := maps.Clone(entry.headers) + headers.Set("Content-Length", strconv.Itoa(len(entry.data))) + return headers, nil } func (m *Memory) Open(_ context.Context, key Key) (io.ReadCloser, http.Header, error) { @@ -96,7 +99,9 @@ func (m *Memory) Open(_ context.Context, key Key) (io.ReadCloser, http.Header, e return nil, nil, os.ErrNotExist } - return io.NopCloser(bytes.NewReader(entry.data)), entry.headers, nil + headers := maps.Clone(entry.headers) + headers.Set("Content-Length", strconv.Itoa(len(entry.data))) + return io.NopCloser(bytes.NewReader(entry.data)), headers, nil } func (m *Memory) Create(ctx context.Context, key Key, headers http.Header, ttl time.Duration) (Writer, error) { diff --git a/internal/cache/s3.go b/internal/cache/s3.go index 6042e2ac..4222ee4c 100644 --- a/internal/cache/s3.go +++ b/internal/cache/s3.go @@ -162,8 +162,12 @@ func (s *S3) statAndHeaders(ctx context.Context, key Key) (minio.ObjectInfo, htt } func (s *S3) Stat(ctx context.Context, key Key) (http.Header, error) { - _, headers, err := s.statAndHeaders(ctx, key) - return headers, err + objInfo, headers, err := s.statAndHeaders(ctx, key) + if err != nil { + return nil, err + } + headers.Set("Content-Length", strconv.FormatInt(objInfo.Size, 10)) + return headers, nil } func (s *S3) Open(ctx context.Context, key Key) (io.ReadCloser, http.Header, error) {