Skip to content

Commit df0b6d3

Browse files
flavorjoneswillnorris
authored andcommitted
fix: return a more-complete 304 from TransformingTransport.RoundTrip
to address the segfault we're seeing in production. The necessary conditions for the issue are: 1. The 303 must return a location URI that changes every time, so that the real location of the image is never cached. As an example, if the remove service redirects to S3, the presence of the `X-Amz-Security-Token` accomplishes this. 2. The image responses must match by Etag, so that imageProxy.should304() returns true causing TranformingTransport.RoundTrip() to return a bare 304 response. If those conditions are met, then the bare 304 Response returned will be used and read by one of the callers. Specifically, the lack of a Body causes a segfault. So let's make it more like a real Response and use http.NoBody so when it's used it doesn't cause things to explode.
1 parent 2254a1f commit df0b6d3

File tree

2 files changed

+80
-7
lines changed

2 files changed

+80
-7
lines changed

imageproxy.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,14 @@ func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, er
573573

574574
if should304(req, resp) {
575575
// bare 304 response, full response will be used from cache
576-
return &http.Response{StatusCode: http.StatusNotModified}, nil
576+
return &http.Response{
577+
Proto: "HTTP/1.1",
578+
ProtoMajor: 1,
579+
ProtoMinor: 1,
580+
Status: fmt.Sprintf("%d %s", http.StatusNotModified, http.StatusText(http.StatusNotModified)),
581+
StatusCode: http.StatusNotModified,
582+
Body: http.NoBody,
583+
}, nil
577584
}
578585

579586
b, err := io.ReadAll(resp.Body)

imageproxy_test.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import (
2323
"strings"
2424
"testing"
2525
"time"
26+
27+
"github.com/die-net/lrucache"
28+
"github.com/google/uuid"
29+
"github.com/gregjones/httpcache"
2630
)
2731

2832
func TestPeekContentType(t *testing.T) {
@@ -332,9 +336,11 @@ func TestShould304(t *testing.T) {
332336

333337
// testTransport is an http.RoundTripper that returns certained canned
334338
// responses for particular requests.
335-
type testTransport struct{}
339+
type testTransport struct {
340+
replyNotModified bool
341+
}
336342

337-
func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
343+
func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
338344
var raw string
339345

340346
switch req.URL.Path {
@@ -352,6 +358,19 @@ func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
352358
_ = png.Encode(img, m)
353359

354360
raw = fmt.Sprintf("HTTP/1.1 200 OK\nContent-Length: %d\nContent-Type: image/png\n\n%s", len(img.Bytes()), img.Bytes())
361+
case "/redirect-to-notmodified":
362+
parts := []string{
363+
"HTTP/1.1 303\nLocation: http://notmodified.test/notmodified?X-Security-Token=",
364+
uuid.NewString(),
365+
"#_=_\nCache-Control: no-store\n\n",
366+
}
367+
raw = strings.Join(parts, "")
368+
case "/notmodified":
369+
if t.replyNotModified {
370+
raw = "HTTP/1.1 304 Not modified\nEtag: \"abcdef\"\n\n"
371+
} else {
372+
raw = "HTTP/1.1 200 OK\nEtag: \"abcdef\"\n\nOriginal response\n"
373+
}
355374
default:
356375
redirectRegexp := regexp.MustCompile(`/redirects-(\d+)`)
357376
if redirectRegexp.MatchString(req.URL.Path) {
@@ -526,7 +545,7 @@ func TestProxy_UpdateCacheHeaders(t *testing.T) {
526545
func TestProxy_ServeHTTP(t *testing.T) {
527546
p := &Proxy{
528547
Client: &http.Client{
529-
Transport: testTransport{},
548+
Transport: &testTransport{},
530549
},
531550
AllowHosts: []string{"good.test"},
532551
ContentTypes: []string{"image/*"},
@@ -564,7 +583,7 @@ func TestProxy_ServeHTTP(t *testing.T) {
564583
func TestProxy_ServeHTTP_is304(t *testing.T) {
565584
p := &Proxy{
566585
Client: &http.Client{
567-
Transport: testTransport{},
586+
Transport: &testTransport{},
568587
},
569588
}
570589

@@ -581,10 +600,57 @@ func TestProxy_ServeHTTP_is304(t *testing.T) {
581600
}
582601
}
583602

603+
func TestProxy_ServeHTTP_cached304(t *testing.T) {
604+
cache := lrucache.New(1024*1024*8, 0)
605+
client := new(http.Client)
606+
tt := testTransport{}
607+
client.Transport = &httpcache.Transport{
608+
Transport: &TransformingTransport{
609+
Transport: &tt,
610+
CachingClient: client,
611+
},
612+
Cache: cache,
613+
MarkCachedResponses: true,
614+
}
615+
616+
p := &Proxy{
617+
Client: client,
618+
FollowRedirects: true,
619+
}
620+
621+
// prime the cache
622+
req := httptest.NewRequest("GET", "http://localhost//http://good.test/redirect-to-notmodified", nil)
623+
recorder := httptest.NewRecorder()
624+
p.ServeHTTP(recorder, req)
625+
626+
resp := recorder.Result()
627+
if got, want := resp.StatusCode, http.StatusOK; got != want {
628+
t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want)
629+
}
630+
if _, found := cache.Get("http://good.test/redirect-to-notmodified#0x0"); !found {
631+
t.Errorf("Response to http://good.test/redirect-to-notmodified#0x0 should be cached")
632+
}
633+
634+
// now make the same request again, but this time make sure the server responds with a 304
635+
tt.replyNotModified = true
636+
req = httptest.NewRequest("GET", "http://localhost//http://good.test/redirect-to-notmodified", nil)
637+
recorder = httptest.NewRecorder()
638+
p.ServeHTTP(recorder, req)
639+
640+
resp = recorder.Result()
641+
if got, want := resp.StatusCode, http.StatusOK; got != want {
642+
t.Errorf("ServeHTTP(%v) returned status %d, want %d", req, got, want)
643+
}
644+
645+
if recorder.Body.String() != "Original response\n" {
646+
t.Errorf("Response isn't what we expected: %v", recorder.Body.String())
647+
}
648+
}
649+
584650
func TestProxy_ServeHTTP_maxRedirects(t *testing.T) {
585651
p := &Proxy{
586652
Client: &http.Client{
587-
Transport: testTransport{},
653+
Transport: &testTransport{},
588654
},
589655
FollowRedirects: true,
590656
}
@@ -658,7 +724,7 @@ func TestProxy_log_default(t *testing.T) {
658724
func TestTransformingTransport(t *testing.T) {
659725
client := new(http.Client)
660726
tr := &TransformingTransport{
661-
Transport: testTransport{},
727+
Transport: &testTransport{},
662728
CachingClient: client,
663729
}
664730
client.Transport = tr

0 commit comments

Comments
 (0)