From 37d6e29e84306ce9470e00fa58a2aa5e48a8759c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 16 Aug 2026 18:41:43 +0100 Subject: [PATCH] Preserve partial-response byte ranges --- .../subsystems/performance-and-scalability.md | 15 ++++++---- pkg/server/gzip_middleware.go | 3 +- pkg/server/gzip_middleware_test.go | 28 +++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 7d5500c840..8540ddaf79 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -204,9 +204,11 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. exclusions, and bypass token-valid WebSocket upgrades before wrapping the response writer. Informational responses must leave negotiation open for the final response, body-forbidden statuses must not gain compression - framing, and once a body-permitted successful response advertises gzip, an - explicit empty body must still emit a valid empty gzip member. Regression - proof lives in `pkg/server/gzip_middleware_test.go`. + framing, and partial responses must preserve the identity representation + whose byte positions `Content-Range` describes. Once a body-permitted full + response advertises gzip, an explicit empty body must still emit a valid + empty gzip member. Regression proof lives in + `pkg/server/gzip_middleware_test.go`. WebSocket state refreshes on monitor, agent-report, and alert mutation hot paths must enqueue coalesced current-state invalidations instead of building full frontend-state payloads at every signal. The hub owns delayed @@ -1016,9 +1018,10 @@ supports wildcard acceptance, applies strict three-digit qvalue parsing, and lets explicit gzip exclusions override wildcards. WebSocket token lists and range requests bypass the wrapper. Explicit empty 200 responses carry a valid empty gzip stream whenever `Content-Encoding: gzip` is committed, while -informational responses defer negotiation to the final response and 204/205/304, -SSE, already encoded, small declared, and binary responses stay on their -identity paths. +informational responses defer negotiation to the final response and +204/205/206/304, SSE, already encoded, small declared, and binary responses +stay on their identity paths. Partial content retains the identity bytes that +its `Content-Range` describes. ### Libvirt workload projection stays bounded and allocation-local diff --git a/pkg/server/gzip_middleware.go b/pkg/server/gzip_middleware.go index 0d4ae01378..3248c3af22 100644 --- a/pkg/server/gzip_middleware.go +++ b/pkg/server/gzip_middleware.go @@ -180,7 +180,8 @@ func (w *gzipResponseWriter) decide(status int) { header := w.Header() header.Add("Vary", "Accept-Encoding") - if status == http.StatusNoContent || status == http.StatusResetContent || status == http.StatusNotModified { + switch status { + case http.StatusNoContent, http.StatusResetContent, http.StatusPartialContent, http.StatusNotModified: return } if header.Get("Content-Encoding") != "" { diff --git a/pkg/server/gzip_middleware_test.go b/pkg/server/gzip_middleware_test.go index 76422525ac..cf054bfd74 100644 --- a/pkg/server/gzip_middleware_test.go +++ b/pkg/server/gzip_middleware_test.go @@ -358,6 +358,34 @@ func TestWithGzipPreservesStatusAndEmptyBodies(t *testing.T) { } } +func TestWithGzipPreservesPartialContentRepresentation(t *testing.T) { + payload := strings.Repeat("x", 1024) + handler := withGzip(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Range", "bytes 0-1023/2048") + w.WriteHeader(http.StatusPartialContent) + _, _ = w.Write([]byte(payload)) + })) + + req := httptest.NewRequest(http.MethodGet, "/api/export?segment=first", nil) + req.Header.Set("Accept-Encoding", "gzip") + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusPartialContent { + t.Fatalf("status = %d, want 206", rec.Code) + } + if got := rec.Header().Get("Content-Encoding"); got != "" { + t.Fatalf("Content-Encoding = %q, want identity for Content-Range semantics", got) + } + if got := rec.Header().Get("Content-Range"); got != "bytes 0-1023/2048" { + t.Fatalf("Content-Range = %q, want original byte range", got) + } + if rec.Body.String() != payload { + t.Fatalf("partial response body was transformed") + } +} + func TestWithGzipDefersDecisionPastInformationalResponse(t *testing.T) { payload := strings.Repeat(`{"status":"ready"}`, 256) handler := withGzip(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {