package httpfetcher import ( "errors" "io" "net/http" "net/http/httptest" "sync" "testing" ) // TestFetch_SendsConditionalHeadersAndHandlesNotModified checks that Fetch // sends a non-empty ifNoneMatch and ifModifiedSince as request headers, that // upstream's 304 to such a request is a result with status 304 and no // content, and that a 304 to a request with neither is ErrUpstreamError. func TestFetch_SendsConditionalHeadersAndHandlesNotModified(t *testing.T) { t.Parallel() const ( etag = `"abc"` lastModified = "Mon, 02 Nov 2026 10:00:00 GMT" ) var ( mu sync.Mutex received http.Header ) srv := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { mu.Lock() received = r.Header.Clone() mu.Unlock() w.WriteHeader(http.StatusNotModified) })) t.Cleanup(srv.Close) f, _ := newServerFetcher(t, srv, nil) res, err := f.Fetch(testContext(t), upstreamSource("/image"), etag, lastModified) if err != nil { t.Fatalf("conditional Fetch() error = %v", err) } body, err := io.ReadAll(res.Content) _ = res.Content.Close() mu.Lock() t.Logf("status = %d, body = %q, request headers = %v", res.StatusCode, body, received) gotIfNoneMatch := received.Get("If-None-Match") gotIfModifiedSince := received.Get("If-Modified-Since") mu.Unlock() if err != nil || len(body) != 0 { t.Errorf("304 content = %q, error = %v; want empty", body, err) } if res.StatusCode != http.StatusNotModified { t.Errorf("StatusCode = %d, want 304", res.StatusCode) } if gotIfNoneMatch != etag || gotIfModifiedSince != lastModified { t.Errorf("If-None-Match = %q, If-Modified-Since = %q; want %q and %q", gotIfNoneMatch, gotIfModifiedSince, etag, lastModified) } if held := len(f.allHostsSemaphore); held != 0 { t.Errorf("connection still held after the 304: %d held", held) } _, err = f.Fetch(testContext(t), upstreamSource("/image"), "", "") if !errors.Is(err, ErrUpstreamError) { t.Errorf("unconditional Fetch() answered 304: error = %v, want ErrUpstreamError", err) } }