Browse Source
S3: Fix Content-Encoding header not preserved (#7894) (#7895)
S3: Fix Content-Encoding header not preserved (#7894) (#7895)
* S3: Fix Content-Encoding header not preserved (#7894) The Content-Encoding header was not being returned in S3 GET/HEAD responses because it wasn't being stored in metadata during PUT operations. Root cause: The putToFiler function only stored a hardcoded list of standard HTTP headers (Cache-Control, Expires, Content-Disposition) but was missing Content-Encoding and Content-Language. Fix: Added Content-Encoding and Content-Language to the list of standard headers that are stored in entry.Extended during PUT operations. This matches the behavior of ParseS3Metadata (used for multipart uploads) and ensures consistency across all S3 operations. Fixes #7894 * Update s3api_object_handlers_put.gopull/2932/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 209 additions and 2 deletions
@ -0,0 +1,204 @@ |
|||||
|
package s3api |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"net/http/httptest" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
// TestContentEncodingPreservation tests that Content-Encoding and Content-Language headers
|
||||
|
// are preserved during S3 PUT and GET operations.
|
||||
|
// This is a regression test for issue #7894
|
||||
|
func TestContentEncodingPreservation(t *testing.T) { |
||||
|
testCases := []struct { |
||||
|
name string |
||||
|
contentEncoding string |
||||
|
contentLanguage string |
||||
|
body string |
||||
|
}{ |
||||
|
{ |
||||
|
name: "gzip encoding", |
||||
|
contentEncoding: "gzip", |
||||
|
contentLanguage: "", |
||||
|
body: "Hello, SeaweedFS with gzip!", |
||||
|
}, |
||||
|
{ |
||||
|
name: "zstd encoding", |
||||
|
contentEncoding: "zstd", |
||||
|
contentLanguage: "", |
||||
|
body: "Hello, SeaweedFS with zstd!", |
||||
|
}, |
||||
|
{ |
||||
|
name: "deflate encoding", |
||||
|
contentEncoding: "deflate", |
||||
|
contentLanguage: "", |
||||
|
body: "Hello, SeaweedFS with deflate!", |
||||
|
}, |
||||
|
{ |
||||
|
name: "br (Brotli) encoding", |
||||
|
contentEncoding: "br", |
||||
|
contentLanguage: "", |
||||
|
body: "Hello, SeaweedFS with Brotli!", |
||||
|
}, |
||||
|
{ |
||||
|
name: "multiple encodings", |
||||
|
contentEncoding: "gzip, deflate", |
||||
|
contentLanguage: "", |
||||
|
body: "Hello, SeaweedFS with multiple encodings!", |
||||
|
}, |
||||
|
{ |
||||
|
name: "encoding with language", |
||||
|
contentEncoding: "gzip", |
||||
|
contentLanguage: "en-US", |
||||
|
body: "Hello, SeaweedFS with language!", |
||||
|
}, |
||||
|
{ |
||||
|
name: "language only", |
||||
|
contentEncoding: "", |
||||
|
contentLanguage: "fr-FR", |
||||
|
body: "Bonjour, SeaweedFS!", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, tc := range testCases { |
||||
|
t.Run(tc.name, func(t *testing.T) { |
||||
|
// Create a mock S3 API server
|
||||
|
s3a := &S3ApiServer{ |
||||
|
option: &S3ApiServerOption{ |
||||
|
BucketsPath: "/tmp/test-buckets", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
bucket := "test-bucket" |
||||
|
key := "test-object.txt" |
||||
|
|
||||
|
// Create PUT request with Content-Encoding and/or Content-Language headers
|
||||
|
putReq := httptest.NewRequest("PUT", "/"+bucket+"/"+key, bytes.NewBufferString(tc.body)) |
||||
|
putReq.Header.Set("Content-Type", "text/plain") |
||||
|
if tc.contentEncoding != "" { |
||||
|
putReq.Header.Set("Content-Encoding", tc.contentEncoding) |
||||
|
} |
||||
|
if tc.contentLanguage != "" { |
||||
|
putReq.Header.Set("Content-Language", tc.contentLanguage) |
||||
|
} |
||||
|
|
||||
|
// Test that ParseS3Metadata correctly extracts the headers
|
||||
|
metadata, errCode := ParseS3Metadata(putReq, nil, false) |
||||
|
require.Equal(t, 0, int(errCode), "ParseS3Metadata should succeed") |
||||
|
|
||||
|
// Verify Content-Encoding is stored in metadata
|
||||
|
if tc.contentEncoding != "" { |
||||
|
assert.Equal(t, []byte(tc.contentEncoding), metadata["Content-Encoding"], |
||||
|
"Content-Encoding should be stored in metadata") |
||||
|
} else { |
||||
|
assert.NotContains(t, metadata, "Content-Encoding", |
||||
|
"Content-Encoding should not be in metadata when not provided") |
||||
|
} |
||||
|
|
||||
|
// Verify Content-Language is stored in metadata
|
||||
|
if tc.contentLanguage != "" { |
||||
|
assert.Equal(t, []byte(tc.contentLanguage), metadata["Content-Language"], |
||||
|
"Content-Language should be stored in metadata") |
||||
|
} else { |
||||
|
assert.NotContains(t, metadata, "Content-Language", |
||||
|
"Content-Language should not be in metadata when not provided") |
||||
|
} |
||||
|
|
||||
|
// Simulate GET response - verify headers are set correctly
|
||||
|
getResp := httptest.NewRecorder() |
||||
|
getReq := httptest.NewRequest("GET", "/"+bucket+"/"+key, nil) |
||||
|
|
||||
|
// Create a mock entry with the metadata
|
||||
|
entry := &filer_pb.Entry{ |
||||
|
Name: key, |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 1234567890, |
||||
|
FileSize: uint64(len(tc.body)), |
||||
|
Mime: "text/plain", |
||||
|
}, |
||||
|
Extended: metadata, |
||||
|
} |
||||
|
|
||||
|
// Call setResponseHeaders to set headers from metadata
|
||||
|
s3a.setResponseHeaders(getResp, getReq, entry, int64(len(tc.body))) |
||||
|
|
||||
|
// Verify Content-Encoding header is returned
|
||||
|
if tc.contentEncoding != "" { |
||||
|
actualEncoding := getResp.Header().Get("Content-Encoding") |
||||
|
assert.Equal(t, tc.contentEncoding, actualEncoding, |
||||
|
"Content-Encoding header should be preserved in GET response") |
||||
|
} else { |
||||
|
assert.Empty(t, getResp.Header().Get("Content-Encoding"), |
||||
|
"Content-Encoding should not be set when not provided") |
||||
|
} |
||||
|
|
||||
|
// Verify Content-Language header is returned
|
||||
|
if tc.contentLanguage != "" { |
||||
|
actualLanguage := getResp.Header().Get("Content-Language") |
||||
|
assert.Equal(t, tc.contentLanguage, actualLanguage, |
||||
|
"Content-Language header should be preserved in GET response") |
||||
|
} else { |
||||
|
assert.Empty(t, getResp.Header().Get("Content-Language"), |
||||
|
"Content-Language should not be set when not provided") |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestContentEncodingWithOtherHeaders verifies that Content-Encoding works
|
||||
|
// correctly alongside other standard headers
|
||||
|
func TestContentEncodingWithOtherHeaders(t *testing.T) { |
||||
|
s3a := &S3ApiServer{ |
||||
|
option: &S3ApiServerOption{ |
||||
|
BucketsPath: "/tmp/test-buckets", |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
bucket := "test-bucket" |
||||
|
key := "test-object.txt" |
||||
|
body := "Test content" |
||||
|
|
||||
|
// Create PUT request with multiple headers
|
||||
|
putReq := httptest.NewRequest("PUT", "/"+bucket+"/"+key, bytes.NewBufferString(body)) |
||||
|
putReq.Header.Set("Content-Type", "text/plain") |
||||
|
putReq.Header.Set("Content-Encoding", "gzip") |
||||
|
putReq.Header.Set("Content-Language", "en-US") |
||||
|
putReq.Header.Set("Cache-Control", "max-age=3600") |
||||
|
putReq.Header.Set("Content-Disposition", "attachment; filename=test.txt") |
||||
|
|
||||
|
// Parse metadata
|
||||
|
metadata, errCode := ParseS3Metadata(putReq, nil, false) |
||||
|
require.Equal(t, 0, int(errCode)) |
||||
|
|
||||
|
// Verify all headers are stored
|
||||
|
assert.Equal(t, []byte("gzip"), metadata["Content-Encoding"]) |
||||
|
assert.Equal(t, []byte("en-US"), metadata["Content-Language"]) |
||||
|
assert.Equal(t, []byte("max-age=3600"), metadata["Cache-Control"]) |
||||
|
assert.Equal(t, []byte("attachment; filename=test.txt"), metadata["Content-Disposition"]) |
||||
|
|
||||
|
// Simulate GET response
|
||||
|
getResp := httptest.NewRecorder() |
||||
|
getReq := httptest.NewRequest("GET", "/"+bucket+"/"+key, nil) |
||||
|
|
||||
|
entry := &filer_pb.Entry{ |
||||
|
Name: key, |
||||
|
Attributes: &filer_pb.FuseAttributes{ |
||||
|
Mtime: 1234567890, |
||||
|
FileSize: uint64(len(body)), |
||||
|
Mime: "text/plain", |
||||
|
}, |
||||
|
Extended: metadata, |
||||
|
} |
||||
|
|
||||
|
s3a.setResponseHeaders(getResp, getReq, entry, int64(len(body))) |
||||
|
|
||||
|
// Verify all headers are returned
|
||||
|
assert.Equal(t, "gzip", getResp.Header().Get("Content-Encoding")) |
||||
|
assert.Equal(t, "en-US", getResp.Header().Get("Content-Language")) |
||||
|
assert.Equal(t, "max-age=3600", getResp.Header().Get("Cache-Control")) |
||||
|
assert.Equal(t, "attachment; filename=test.txt", getResp.Header().Get("Content-Disposition")) |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue