From bbd21238359590dac794deb3125653cc483b0030 Mon Sep 17 00:00:00 2001 From: chrislu Date: Mon, 17 Nov 2025 16:55:32 -0800 Subject: [PATCH] Incomplete HTTP Response Error Handling --- weed/s3api/s3_sse_ctr_test.go | 27 ++++++------ weed/s3api/s3api_object_handlers.go | 64 +++++++++++++++++++---------- 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/weed/s3api/s3_sse_ctr_test.go b/weed/s3api/s3_sse_ctr_test.go index 299f837dc..81bbaf003 100644 --- a/weed/s3api/s3_sse_ctr_test.go +++ b/weed/s3api/s3_sse_ctr_test.go @@ -15,10 +15,10 @@ func TestCalculateIVWithOffset(t *testing.T) { rand.Read(baseIV) tests := []struct { - name string - offset int64 - expectedSkip int - expectedBlock int64 + name string + offset int64 + expectedSkip int + expectedBlock int64 }{ {"BlockAligned_0", 0, 0, 0}, {"BlockAligned_16", 16, 0, 1}, @@ -108,7 +108,7 @@ func TestCTRDecryptionWithNonBlockAlignedOffset(t *testing.T) { } decryptStream := cipher.NewCTR(decryptBlock, adjustedIV) - + // Create a reader for the ciphertext starting at block-aligned offset ciphertextFromBlockStart := ciphertext[blockAlignedOffset:] decryptedFromBlockStart := make([]byte, len(ciphertextFromBlockStart)) @@ -139,7 +139,7 @@ func TestCTRDecryptionWithNonBlockAlignedOffset(t *testing.T) { previewLen2 = len(decryptedFromOffset) } t.Errorf(" Got first 32 bytes: %x", decryptedFromOffset[:previewLen2]) - + // Find first mismatch for i := 0; i < len(expectedPlaintext) && i < len(decryptedFromOffset); i++ { if expectedPlaintext[i] != decryptedFromOffset[i] { @@ -184,9 +184,9 @@ func TestCTRRangeRequestSimulation(t *testing.T) { }{ {"First byte", 0, 0}, {"First 100 bytes", 0, 99}, - {"Mid-block range", 5, 100}, // Critical: starts at non-aligned offset - {"Single mid-block byte", 17, 17}, // Critical: single byte at offset 17 - {"Cross-block range", 10, 50}, // Spans multiple blocks + {"Mid-block range", 5, 100}, // Critical: starts at non-aligned offset + {"Single mid-block byte", 17, 17}, // Critical: single byte at offset 17 + {"Cross-block range", 10, 50}, // Spans multiple blocks {"Large range", 1000, 10000}, {"Tail range", int64(objectSize - 1000), int64(objectSize - 1)}, } @@ -194,7 +194,7 @@ func TestCTRRangeRequestSimulation(t *testing.T) { for _, rt := range rangeTests { t.Run(rt.name, func(t *testing.T) { rangeSize := rt.end - rt.start + 1 - + // Calculate adjusted IV and skip for the range start adjustedIV, skip := calculateIVWithOffset(iv, rt.start) @@ -208,7 +208,7 @@ func TestCTRRangeRequestSimulation(t *testing.T) { } decryptStream := cipher.NewCTR(decryptBlock, adjustedIV) - + // Decrypt from block-aligned start through the end of range ciphertextFromBlock := ciphertext[blockAlignedStart : rt.end+1] decryptedFromBlock := make([]byte, len(ciphertextFromBlock)) @@ -224,7 +224,7 @@ func TestCTRRangeRequestSimulation(t *testing.T) { // Verify decrypted range matches original plaintext expectedPlaintext := plaintext[rt.start : rt.end+1] if !bytes.Equal(decryptedRange, expectedPlaintext) { - t.Errorf("Range decryption mismatch for %s (offset=%d, size=%d, skip=%d)", + t.Errorf("Range decryption mismatch for %s (offset=%d, size=%d, skip=%d)", rt.name, rt.start, rangeSize, skip) previewLen := 64 if len(expectedPlaintext) < previewLen { @@ -244,7 +244,7 @@ func TestCTRRangeRequestSimulation(t *testing.T) { // TestCTRDecryptionWithIOReader tests the integration with io.Reader func TestCTRDecryptionWithIOReader(t *testing.T) { plaintext := []byte("Hello, World! This is a test of CTR mode decryption with non-aligned offsets.") - + key := make([]byte, 32) iv := make([]byte, 16) rand.Read(key) @@ -305,4 +305,3 @@ func TestCTRDecryptionWithIOReader(t *testing.T) { }) } } - diff --git a/weed/s3api/s3api_object_handlers.go b/weed/s3api/s3api_object_handlers.go index 04e0f35d8..b53af4847 100644 --- a/weed/s3api/s3api_object_handlers.go +++ b/weed/s3api/s3api_object_handlers.go @@ -578,7 +578,10 @@ func (s3a *S3ApiServer) GetObjectHandler(w http.ResponseWriter, r *http.Request) streamTime = time.Since(tStream) if err != nil { glog.Errorf("GetObjectHandler: failed to stream from volume servers: %v", err) - // Don't write error response - headers already sent + // Try to write error response. The HTTP library will gracefully handle cases + // where headers are already sent (e.g., during streaming errors). + // For early validation errors, this ensures client gets proper error response. + s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) return } } @@ -602,6 +605,9 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R }() if entry == nil { + // Early validation error: write proper HTTP response + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Internal Server Error: entry is nil") return fmt.Errorf("entry is nil") } @@ -689,43 +695,59 @@ func (s3a *S3ApiServer) streamFromVolumeServers(w http.ResponseWriter, r *http.R } rangeParseTime = time.Since(tRangeParse) - // Set standard HTTP headers from entry metadata - // IMPORTANT: Set ALL headers BEFORE calling WriteHeader (headers are ignored after WriteHeader) - tHeaderSet := time.Now() - s3a.setResponseHeaders(w, entry, totalSize) - - // Override/add range-specific headers if this is a range request - if isRangeRequest { - 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)) - } - headerSetTime = time.Since(tHeaderSet) - - // Now write status code (headers are all set) - if isRangeRequest { - w.WriteHeader(http.StatusPartialContent) - } - - // For small files stored inline in entry.Content + // For small files stored inline in entry.Content - validate BEFORE setting headers if len(entry.Content) > 0 && totalSize == int64(len(entry.Content)) { if isRangeRequest { - // Safely convert int64 to int for slice indexing + // Safely convert int64 to int for slice indexing - validate BEFORE WriteHeader if offset < 0 || offset > int64(math.MaxInt) || size < 0 || size > int64(math.MaxInt) { + // Early validation error: write proper HTTP response BEFORE headers + w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", totalSize)) + w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + fmt.Fprintf(w, "Range too large for platform: offset=%d, size=%d", offset, size) return fmt.Errorf("range too large for platform: offset=%d, size=%d", offset, size) } start := int(offset) end := start + int(size) - // Bounds check (should already be validated, but double-check) + // Bounds check (should already be validated, but double-check) - BEFORE WriteHeader if start < 0 || start > len(entry.Content) || end > len(entry.Content) || end < start { + // Early validation error: write proper HTTP response BEFORE headers + w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", totalSize)) + w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) + fmt.Fprintf(w, "Invalid range for inline content: start=%d, end=%d, len=%d)", start, end, len(entry.Content)) return 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 + s3a.setResponseHeaders(w, entry, 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.WriteHeader(http.StatusPartialContent) _, err := w.Write(entry.Content[start:end]) return err } + // Non-range request for inline content + s3a.setResponseHeaders(w, entry, totalSize) + w.WriteHeader(http.StatusOK) _, err := w.Write(entry.Content) return err } + // Set standard HTTP headers from entry metadata + // IMPORTANT: Set ALL headers BEFORE calling WriteHeader (headers are ignored after WriteHeader) + tHeaderSet := time.Now() + s3a.setResponseHeaders(w, entry, totalSize) + + // Override/add range-specific headers if this is a range request + if isRangeRequest { + 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)) + } + headerSetTime = time.Since(tHeaderSet) + + // Now write status code (headers are all set) + if isRangeRequest { + w.WriteHeader(http.StatusPartialContent) + } + // Get chunks chunks := entry.GetChunks() glog.Infof("streamFromVolumeServers: entry has %d chunks, totalSize=%d, isRange=%v, offset=%d, size=%d",