Skip to content

Commit f07eb9e

Browse files
authored
fix: [proxy/engines] correct off-by-one byte ranges and handle ignored io.ReadAll errors (#948)
Signed-off-by: Chris Randles <randles.chris@gmail.com>
1 parent 44b0e7e commit f07eb9e

File tree

5 files changed

+196
-6
lines changed

5 files changed

+196
-6
lines changed

pkg/proxy/engines/document.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func (d *HTTPDocument) getByteRanges() byterange.Ranges {
137137
} else if ranges := d.RangeParts.Ranges(); len(ranges) > 0 {
138138
return ranges
139139
}
140-
return byterange.Ranges{byterange.Range{Start: 0, End: d.ContentLength}}
140+
return byterange.Ranges{byterange.Range{Start: 0, End: d.ContentLength - 1}}
141141
}
142142

143143
// ShallowCopy returns a shallow copy of the HTTPDocument with a fresh headerLock.

pkg/proxy/engines/document_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,66 @@ func TestLoadRangeParts(t *testing.T) {
235235
}
236236
}
237237

238+
func TestGetByteRanges(t *testing.T) {
239+
tests := []struct {
240+
name string
241+
doc *HTTPDocument
242+
wantStart int64
243+
wantEnd int64
244+
wantLen int
245+
}{
246+
{
247+
name: "returns stored ranges when present",
248+
doc: &HTTPDocument{
249+
ContentLength: 100,
250+
Ranges: byterange.Ranges{{Start: 10, End: 20}},
251+
},
252+
wantStart: 10,
253+
wantEnd: 20,
254+
wantLen: 1,
255+
},
256+
{
257+
name: "returns range parts ranges when no stored ranges",
258+
doc: &HTTPDocument{
259+
ContentLength: 100,
260+
RangeParts: byterange.MultipartByteRanges{
261+
byterange.Range{Start: 5, End: 15}: &byterange.MultipartByteRange{
262+
Range: byterange.Range{Start: 5, End: 15},
263+
Content: make([]byte, 11),
264+
},
265+
},
266+
},
267+
wantStart: 5,
268+
wantEnd: 15,
269+
wantLen: 1,
270+
},
271+
{
272+
name: "returns full content range with inclusive end",
273+
doc: &HTTPDocument{
274+
ContentLength: 100,
275+
},
276+
wantStart: 0,
277+
wantEnd: 99, // ContentLength - 1, not ContentLength
278+
wantLen: 1,
279+
},
280+
}
281+
282+
for _, tt := range tests {
283+
t.Run(tt.name, func(t *testing.T) {
284+
ranges := tt.doc.getByteRanges()
285+
if len(ranges) != tt.wantLen {
286+
t.Fatalf("expected %d ranges, got %d", tt.wantLen, len(ranges))
287+
}
288+
if ranges[0].Start != tt.wantStart {
289+
t.Errorf("expected Start=%d, got %d", tt.wantStart, ranges[0].Start)
290+
}
291+
if ranges[0].End != tt.wantEnd {
292+
t.Errorf("expected End=%d, got %d", tt.wantEnd, ranges[0].End)
293+
}
294+
})
295+
}
296+
}
297+
238298
func TestGetByterangeChunkBody(t *testing.T) {
239299
d := &HTTPDocument{
240300
Body: []byte{0, 1, 2, 3, 4, 5},

pkg/proxy/engines/objectproxycache.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ func handleCachePartialHit(pr *proxyRequest) error {
7575
d := pr.cacheDocument
7676
resp := pr.upstreamResponse
7777
if pr.isPartialResponse {
78-
b, _ := io.ReadAll(pr.upstreamReader)
78+
b, err := io.ReadAll(pr.upstreamReader)
79+
if err != nil {
80+
return err
81+
}
7982
d2 := &HTTPDocument{}
8083

8184
d2.ParsePartialContentBody(resp, b)
@@ -86,7 +89,7 @@ func handleCachePartialHit(pr *proxyRequest) error {
8689
d.RangeParts.Merge(d2.RangeParts)
8790
d.Ranges = d.RangeParts.Ranges()
8891
d.StoredRangeParts = d.RangeParts.PackableMultipartByteRanges()
89-
err := d.FulfillContentBody()
92+
err = d.FulfillContentBody()
9093

9194
if err == nil {
9295
pr.upstreamResponse.Body = io.NopCloser(bytes.NewReader(d.Body))

pkg/proxy/engines/proxy_request.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func (pr *proxyRequest) prepareRevalidationRequest() {
203203
if len(pr.wantedRanges) > 0 {
204204
wr = pr.wantedRanges
205205
} else {
206-
wr = byterange.Ranges{{Start: 0, End: cl}}
206+
wr = byterange.Ranges{{Start: 0, End: cl - 1}}
207207
}
208208

209209
// revalRanges are the ranges we have in cache that have expired, but the user needs
@@ -660,7 +660,12 @@ func (pr *proxyRequest) reconstituteResponses() {
660660
wg.Go(func() {
661661
// oh snap. so we have some partial content to merge in, but the original cache document
662662
// is now invalid. lets go ahead and reset it.
663-
b, _ := io.ReadAll(resp.Body)
663+
b, err := io.ReadAll(resp.Body)
664+
if err != nil {
665+
logger.Error("error reading revalidation response body",
666+
logging.Pairs{"detail": err.Error()})
667+
return
668+
}
664669
appendLock.Lock()
665670
parts.ParsePartialContentBody(resp, b)
666671
appendLock.Unlock()
@@ -682,7 +687,12 @@ func (pr *proxyRequest) reconstituteResponses() {
682687
appendLock.Unlock()
683688

684689
if resp.StatusCode == http.StatusPartialContent {
685-
b, _ := io.ReadAll(resp.Body)
690+
b, err := io.ReadAll(resp.Body)
691+
if err != nil {
692+
logger.Error("error reading origin response body",
693+
logging.Pairs{"detail": err.Error()})
694+
return
695+
}
686696
appendLock.Lock()
687697
parts.ParsePartialContentBody(resp, b)
688698
appendLock.Unlock()

pkg/proxy/engines/proxy_request_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package engines
1818

1919
import (
2020
"bytes"
21+
"errors"
22+
"io"
2123
"net/http"
2224
"sync"
2325
"testing"
@@ -338,6 +340,37 @@ func TestPrepareRevalidationRequestNoRange(t *testing.T) {
338340
}
339341
}
340342

343+
func TestPrepareRevalidationRequestDefaultRangeInclusiveEnd(t *testing.T) {
344+
logger.SetLogger(logging.ConsoleLogger(level.Error))
345+
r, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
346+
347+
o := &bo.Options{DearticulateUpstreamRanges: true}
348+
r = request.SetResources(r, request.NewResources(o, nil, nil, nil, nil, nil))
349+
350+
pr := proxyRequest{
351+
Request: r,
352+
upstreamRequest: r,
353+
cachingPolicy: &CachingPolicy{},
354+
upstreamResponse: &http.Response{},
355+
cacheDocument: &HTTPDocument{
356+
ContentLength: 100,
357+
Ranges: byterange.Ranges{byterange.Range{Start: 30, End: 40}},
358+
},
359+
cacheStatus: status.LookupStatusPartialHit,
360+
// no wantedRanges — triggers default range using ContentLength
361+
}
362+
pr.prepareRevalidationRequest()
363+
364+
// with no wantedRanges, the default wanted range is 0 to ContentLength-1 (99)
365+
// revalRanges = neededRanges.CalculateDeltas(wr, cl) with nil neededRanges gives wr back
366+
// that's 1 range so rh = revalRanges.String() = "bytes=0-99"
367+
v := pr.revalidationRequest.Header.Get(headers.NameRange)
368+
expected := "bytes=0-99"
369+
if v != expected {
370+
t.Errorf("expected %s got %s", expected, v)
371+
}
372+
}
373+
341374
func TestPrepareUpstreamRequests(t *testing.T) {
342375
logger.SetLogger(logging.ConsoleLogger(level.Error))
343376
r, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
@@ -400,3 +433,87 @@ func TestReconstituteResponses(t *testing.T) {
400433
t.Errorf("expected %d got %d", 0, len(pr.originRequests))
401434
}
402435
}
436+
437+
// errReader is an io.ReadCloser that always returns an error
438+
type errReader struct{ err error }
439+
440+
func (e *errReader) Read([]byte) (int, error) { return 0, e.err }
441+
func (e *errReader) Close() error { return nil }
442+
443+
func TestReconstituteResponsesReadError(t *testing.T) {
444+
logger.SetLogger(logging.ConsoleLogger(level.Error))
445+
446+
r1, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
447+
r2, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
448+
baseReq, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
449+
baseReq = request.SetResources(baseReq, request.NewResources(&bo.Options{}, nil, nil, nil, nil, nil))
450+
451+
readErr := errors.New("simulated read error")
452+
453+
pr := &proxyRequest{
454+
mapLock: &sync.Mutex{},
455+
cachingPolicy: &CachingPolicy{},
456+
originRequests: []*http.Request{r1, r2},
457+
originResponses: []*http.Response{
458+
{
459+
StatusCode: http.StatusPartialContent,
460+
Header: http.Header{headers.NameContentRange: []string{"bytes 0-3/10"}},
461+
Body: &errReader{err: readErr},
462+
},
463+
{
464+
StatusCode: http.StatusPartialContent,
465+
Header: http.Header{headers.NameContentRange: []string{"bytes 4-9/10"}},
466+
Body: io.NopCloser(bytes.NewReader([]byte("456789"))),
467+
},
468+
},
469+
cacheDocument: &HTTPDocument{ContentLength: 10},
470+
}
471+
pr.Request = baseReq
472+
473+
// should not panic; the error reader's goroutine logs and returns
474+
pr.reconstituteResponses()
475+
476+
// the second response should still have been processed
477+
if pr.upstreamResponse == nil {
478+
t.Error("expected upstream response to be set")
479+
}
480+
}
481+
482+
func TestReconstituteResponsesRevalidationReadError(t *testing.T) {
483+
logger.SetLogger(logging.ConsoleLogger(level.Error))
484+
485+
r1, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
486+
reval, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
487+
baseReq, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/", nil)
488+
baseReq = request.SetResources(baseReq, request.NewResources(&bo.Options{}, nil, nil, nil, nil, nil))
489+
490+
readErr := errors.New("simulated revalidation read error")
491+
492+
pr := &proxyRequest{
493+
mapLock: &sync.Mutex{},
494+
cachingPolicy: &CachingPolicy{},
495+
revalidationRequest: reval,
496+
revalidationResponse: &http.Response{
497+
StatusCode: http.StatusPartialContent,
498+
Header: http.Header{headers.NameContentRange: []string{"bytes 0-4/10"}},
499+
Body: &errReader{err: readErr},
500+
},
501+
originRequests: []*http.Request{r1},
502+
originResponses: []*http.Response{
503+
{
504+
StatusCode: http.StatusPartialContent,
505+
Header: http.Header{headers.NameContentRange: []string{"bytes 5-9/10"}},
506+
Body: io.NopCloser(bytes.NewReader([]byte("56789"))),
507+
},
508+
},
509+
cacheDocument: &HTTPDocument{ContentLength: 10},
510+
}
511+
pr.Request = baseReq
512+
513+
// should not panic; the revalidation error reader's goroutine logs and returns
514+
pr.reconstituteResponses()
515+
516+
if pr.upstreamResponse == nil {
517+
t.Error("expected upstream response to be set")
518+
}
519+
}

0 commit comments

Comments
 (0)