s3api: Fix response-content-disposition query parameter not being honored (#7559)
* s3api: Fix response-content-disposition query parameter not being honored Fixes #7486 This fix resolves an issue where S3 presigned URLs with query parameters like `response-content-disposition`, `response-content-type`, etc. were being ignored, causing browsers to use default file handling instead of the specified behavior. Changes: - Modified `setResponseHeaders()` to accept the HTTP request object - Added logic to process S3 passthrough headers from query parameters - Updated all call sites to pass the request object - Supports all AWS S3 response override parameters: - response-content-disposition - response-content-type - response-cache-control - response-content-encoding - response-content-language - response-expires The implementation follows the same pattern used in the filer handler and properly honors the AWS S3 API specification for presigned URLs. Testing: - Existing S3 API tests pass without modification - Build succeeds with no compilation errors * Update weed/s3api/s3api_object_handlers.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -880,7 +880,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R
|
|||||||
return newStreamErrorWithResponse(fmt.Errorf("invalid range for inline content: start=%d, end=%d, len=%d", start, end, len(entry.Content)))
|
return newStreamErrorWithResponse(fmt.Errorf("invalid range for inline content: start=%d, end=%d, len=%d", start, end, len(entry.Content)))
|
||||||
}
|
}
|
||||||
// Validation passed - now set headers and write
|
// Validation passed - now set headers and write
|
||||||
s3a.setResponseHeaders(w, entry, totalSize)
|
s3a.setResponseHeaders(w, r, entry, totalSize)
|
||||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, offset+size-1, totalSize))
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, offset+size-1, totalSize))
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||||
w.WriteHeader(http.StatusPartialContent)
|
w.WriteHeader(http.StatusPartialContent)
|
||||||
@@ -888,7 +888,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Non-range request for inline content
|
// Non-range request for inline content
|
||||||
s3a.setResponseHeaders(w, entry, totalSize)
|
s3a.setResponseHeaders(w, r, entry, totalSize)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, err := w.Write(entry.Content)
|
_, err := w.Write(entry.Content)
|
||||||
return err
|
return err
|
||||||
@@ -908,7 +908,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R
|
|||||||
return newStreamErrorWithResponse(fmt.Errorf("data integrity error: size %d reported but no content available", totalSize))
|
return newStreamErrorWithResponse(fmt.Errorf("data integrity error: size %d reported but no content available", totalSize))
|
||||||
}
|
}
|
||||||
// Empty object - set headers and write status
|
// Empty object - set headers and write status
|
||||||
s3a.setResponseHeaders(w, entry, totalSize)
|
s3a.setResponseHeaders(w, r, entry, totalSize)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -958,7 +958,7 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
// All validation and preparation successful - NOW set headers and write status
|
// All validation and preparation successful - NOW set headers and write status
|
||||||
tHeaderSet := time.Now()
|
tHeaderSet := time.Now()
|
||||||
s3a.setResponseHeaders(w, entry, totalSize)
|
s3a.setResponseHeaders(w, r, entry, totalSize)
|
||||||
|
|
||||||
// Override/add range-specific headers if this is a range request
|
// Override/add range-specific headers if this is a range request
|
||||||
if isRangeRequest {
|
if isRangeRequest {
|
||||||
@@ -1164,7 +1164,7 @@ func (s3a *S3ApiServer) streamFromVolumeServersWithSSE(w http.ResponseWriter, r
|
|||||||
// Set response headers
|
// Set response headers
|
||||||
// IMPORTANT: Set ALL headers BEFORE calling WriteHeader (headers are ignored after WriteHeader)
|
// IMPORTANT: Set ALL headers BEFORE calling WriteHeader (headers are ignored after WriteHeader)
|
||||||
tHeaderSet := time.Now()
|
tHeaderSet := time.Now()
|
||||||
s3a.setResponseHeaders(w, entry, totalSize)
|
s3a.setResponseHeaders(w, r, entry, totalSize)
|
||||||
s3a.addSSEResponseHeadersFromEntry(w, r, entry, sseType)
|
s3a.addSSEResponseHeadersFromEntry(w, r, entry, sseType)
|
||||||
|
|
||||||
// Override/add range-specific headers if this is a range request
|
// Override/add range-specific headers if this is a range request
|
||||||
@@ -1894,7 +1894,7 @@ func (s3a *S3ApiServer) addSSEResponseHeadersFromEntry(w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setResponseHeaders sets all standard HTTP response headers from entry metadata
|
// setResponseHeaders sets all standard HTTP response headers from entry metadata
|
||||||
func (s3a *S3ApiServer) setResponseHeaders(w http.ResponseWriter, entry *filer_pb.Entry, totalSize int64) {
|
func (s3a *S3ApiServer) setResponseHeaders(w http.ResponseWriter, r *http.Request, entry *filer_pb.Entry, totalSize int64) {
|
||||||
// Safety check: entry must be valid
|
// Safety check: entry must be valid
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
glog.Errorf("setResponseHeaders: entry is nil")
|
glog.Errorf("setResponseHeaders: entry is nil")
|
||||||
@@ -1974,6 +1974,18 @@ func (s3a *S3ApiServer) setResponseHeaders(w http.ResponseWriter, entry *filer_p
|
|||||||
w.Header().Set(s3_constants.AmzTagCount, strconv.Itoa(tagCount))
|
w.Header().Set(s3_constants.AmzTagCount, strconv.Itoa(tagCount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply S3 passthrough headers from query parameters
|
||||||
|
// AWS S3 supports overriding response headers via query parameters like:
|
||||||
|
// ?response-cache-control=no-cache&response-content-type=application/json
|
||||||
|
// This allows presigned URLs to control how browsers handle the downloaded content
|
||||||
|
if r != nil {
|
||||||
|
for queryParam, headerValue := range r.URL.Query() {
|
||||||
|
if normalizedHeader, ok := s3_constants.PassThroughHeaders[strings.ToLower(queryParam)]; ok && len(headerValue) > 0 && headerValue[0] != "" {
|
||||||
|
w.Header().Set(normalizedHeader, headerValue[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// simpleMasterClient implements the minimal interface for streaming
|
// simpleMasterClient implements the minimal interface for streaming
|
||||||
@@ -2241,7 +2253,7 @@ func (s3a *S3ApiServer) HeadObjectHandler(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
// For HEAD requests, we already have all metadata - just set headers directly
|
// For HEAD requests, we already have all metadata - just set headers directly
|
||||||
totalSize := int64(filer.FileSize(objectEntryForSSE))
|
totalSize := int64(filer.FileSize(objectEntryForSSE))
|
||||||
s3a.setResponseHeaders(w, objectEntryForSSE, totalSize)
|
s3a.setResponseHeaders(w, r, objectEntryForSSE, totalSize)
|
||||||
|
|
||||||
// Check if PartNumber query parameter is present (for multipart objects)
|
// Check if PartNumber query parameter is present (for multipart objects)
|
||||||
// This logic matches the filer handler for consistency
|
// This logic matches the filer handler for consistency
|
||||||
|
|||||||
Reference in New Issue
Block a user