diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 8a3715560..0d27dcf69 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -1182,7 +1182,16 @@ func (s3a *S3ApiServer) setResponseHeaders(w http.ResponseWriter, entry *filer_p for k, v := range entry.Extended { // Skip internal SeaweedFS headers if !strings.HasPrefix(k, "xattr-") && !s3_constants.IsSeaweedFSInternalHeader(k) { - w.Header()[k] = []string{string(v)} + // Support backward compatibility: migrate old lowercase format to canonical format + // OLD: "x-amz-meta-foo" → NEW: "X-Amz-Meta-foo" + headerKey := k + if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") && !strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) { + // Old format detected - normalize to canonical prefix + suffix := strings.ToLower(k[len("x-amz-meta-"):]) + headerKey = s3_constants.AmzUserMetaPrefix + suffix + glog.V(4).Infof("Migrating old user metadata header %q to %q in response", k, headerKey) + } + w.Header()[headerKey] = []string{string(v)} } } } diff --git a/weed/s3api/s3api_object_handlers_copy.go b/weed/s3api/s3api_object_handlers_copy.go index 28f7abcd5..ce65534b4 100644 --- a/weed/s3api/s3api_object_handlers_copy.go +++ b/weed/s3api/s3api_object_handlers_copy.go @@ -700,15 +700,29 @@ func processMetadataBytes(reqHeader http.Header, existing map[string][]byte, rep if replaceMeta { for header, values := range reqHeader { if strings.HasPrefix(header, s3_constants.AmzUserMetaPrefix) { + // AWS S3 stores user metadata keys in lowercase + // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) + // Preserve the canonical prefix "X-Amz-Meta-" but lowercase the user-defined suffix + suffix := strings.ToLower(header[len(s3_constants.AmzUserMetaPrefix):]) + normalizedKey := s3_constants.AmzUserMetaPrefix + suffix for _, value := range values { - metadata[header] = []byte(value) + metadata[normalizedKey] = []byte(value) } } } } else { + // Copy existing metadata as-is (already normalized during storage) + // Support both old format (x-amz-meta-*) and new format (X-Amz-Meta-*) for backward compatibility for k, v := range existing { if strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) { + // New format (X-Amz-Meta-foo) - copy as-is metadata[k] = v + } else if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") { + // Old format (x-amz-meta-foo) - migrate to new format + suffix := strings.ToLower(k[len("x-amz-meta-"):]) + normalizedKey := s3_constants.AmzUserMetaPrefix + suffix + metadata[normalizedKey] = v + glog.V(4).Infof("Migrating old user metadata key %q to %q", k, normalizedKey) } } } diff --git a/weed/s3api/s3api_object_handlers_put.go b/weed/s3api/s3api_object_handlers_put.go index a08d3e04c..d8181f3dd 100644 --- a/weed/s3api/s3api_object_handlers_put.go +++ b/weed/s3api/s3api_object_handlers_put.go @@ -480,12 +480,14 @@ func (s3a *S3ApiServer) putToFiler(r *http.Request, uploadUrl string, dataReader // Copy user metadata and standard headers for k, v := range r.Header { if len(v) > 0 && len(v[0]) > 0 { - if strings.HasPrefix(k, "X-Amz-Meta-") { + if strings.HasPrefix(k, s3_constants.AmzUserMetaPrefix) { // AWS S3 stores user metadata keys in lowercase // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) - // but S3 expects lowercase, so convert back to lowercase for storage - lowerKey := strings.ToLower(k) - entry.Extended[lowerKey] = []byte(v[0]) + // Preserve the canonical prefix "X-Amz-Meta-" but lowercase the user-defined suffix + // This ensures the key is still detectable via prefix checks elsewhere + suffix := strings.ToLower(k[len(s3_constants.AmzUserMetaPrefix):]) + normalizedKey := s3_constants.AmzUserMetaPrefix + suffix + entry.Extended[normalizedKey] = []byte(v[0]) } else if k == "Cache-Control" || k == "Expires" || k == "Content-Disposition" { entry.Extended[k] = []byte(v[0]) } diff --git a/weed/server/filer_server_handlers_write_autochunk.go b/weed/server/filer_server_handlers_write_autochunk.go index 3b9689858..59b83501b 100644 --- a/weed/server/filer_server_handlers_write_autochunk.go +++ b/weed/server/filer_server_handlers_write_autochunk.go @@ -546,10 +546,12 @@ func SaveAmzMetaData(r *http.Request, existing map[string][]byte, isReplace bool if strings.HasPrefix(header, s3_constants.AmzUserMetaPrefix) { // AWS S3 stores user metadata keys in lowercase // Go's HTTP server canonicalizes headers (e.g., x-amz-meta-foo → X-Amz-Meta-Foo) - // but S3 expects lowercase, so convert back to lowercase for storage - lowerHeader := strings.ToLower(header) + // Preserve the canonical prefix "X-Amz-Meta-" but lowercase the user-defined suffix + // This ensures the key is still detectable via prefix checks elsewhere + suffix := strings.ToLower(header[len(s3_constants.AmzUserMetaPrefix):]) + normalizedKey := s3_constants.AmzUserMetaPrefix + suffix for _, value := range values { - metadata[lowerHeader] = []byte(value) + metadata[normalizedKey] = []byte(value) } } }