@@ -16,6 +16,7 @@ package httpcache
1616
1717import (
1818 "errors"
19+ "fmt"
1920 "io"
2021 "log/slog"
2122 "net/http"
@@ -957,3 +958,90 @@ func Test_transport_RevalidationUpdatesCache(t *testing.T) {
957958 })
958959 }
959960}
961+
962+ // Test_transport_Vary_MultipleHeaders verifies that the transport correctly
963+ // handles multiple Vary headers that are sent as separate header lines
964+ // (instead of a single comma-separated line). This is a common scenario in
965+ // practice, and there was a bug where only the first Vary header line was
966+ // being processed, causing incorrect cache hits when subsequent Vary headers
967+ // were not considered.
968+ // Regression test for: https://github.com/bartventer/httpcache/issues/32
969+ func Test_transport_Vary_MultipleHeaders (t * testing.T ) {
970+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
971+ // Simulate server returning multiple separate Vary header lines
972+ // (not comma-separated, but as distinct headers)
973+ w .Header ().Add ("Vary" , "Accept-Language" )
974+ w .Header ().Add ("Vary" , "X-Compatibility-Date" )
975+ w .Header ().Set ("Cache-Control" , "max-age=60" )
976+ w .WriteHeader (http .StatusOK )
977+
978+ lang := r .Header .Get ("Accept-Language" )
979+ date := r .Header .Get ("X-Compatibility-Date" )
980+ _ , _ = fmt .Fprintf (w , "lang=%s date=%s" , lang , date )
981+ }))
982+ defer server .Close ()
983+
984+ c := memcache .Open ()
985+ tr := newTransport (c , WithLogger (slog .New (slog .NewJSONHandler (os .Stderr , & slog.HandlerOptions {
986+ Level : slog .LevelDebug ,
987+ }))))
988+
989+ makeReq := func (lang , date string ) * http.Response {
990+ req , _ := http .NewRequest (http .MethodGet , server .URL , nil )
991+ req .Header .Set ("Accept-Language" , lang )
992+ req .Header .Set ("X-Compatibility-Date" , date )
993+ resp , err := tr .RoundTrip (req )
994+ testutil .RequireNoError (t , err )
995+ return resp
996+ }
997+
998+ // Request 1: MISS — first request for en-us + 2025-01-01
999+ resp := makeReq ("en-us" , "2025-01-01" )
1000+ testutil .AssertEqual (t , http .StatusOK , resp .StatusCode )
1001+ testutil .AssertEqual (
1002+ t ,
1003+ internal .CacheStatusMiss .Value ,
1004+ resp .Header .Get (internal .CacheStatusHeader ),
1005+ )
1006+ body , _ := io .ReadAll (resp .Body )
1007+ _ = resp .Body .Close ()
1008+ testutil .AssertEqual (t , "lang=en-us date=2025-01-01" , string (body ))
1009+
1010+ // Request 2: HIT — same headers
1011+ resp = makeReq ("en-us" , "2025-01-01" )
1012+ testutil .AssertEqual (t , http .StatusOK , resp .StatusCode )
1013+ testutil .AssertEqual (
1014+ t ,
1015+ internal .CacheStatusHit .Value ,
1016+ resp .Header .Get (internal .CacheStatusHeader ),
1017+ )
1018+ body , _ = io .ReadAll (resp .Body )
1019+ _ = resp .Body .Close ()
1020+ testutil .AssertEqual (t , "lang=en-us date=2025-01-01" , string (body ))
1021+
1022+ // Request 3: MISS — X-Compatibility-Date changed, must be a different cache entry
1023+ // This is the bug: without the fix, this incorrectly returns a HIT
1024+ resp = makeReq ("en-us" , "2026-01-01" )
1025+ testutil .AssertEqual (t , http .StatusOK , resp .StatusCode )
1026+ testutil .AssertEqual (
1027+ t ,
1028+ internal .CacheStatusMiss .Value ,
1029+ resp .Header .Get (internal .CacheStatusHeader ),
1030+ "X-Compatibility-Date is a Vary header; different value must produce a cache MISS" ,
1031+ )
1032+ body , _ = io .ReadAll (resp .Body )
1033+ _ = resp .Body .Close ()
1034+ testutil .AssertEqual (t , "lang=en-us date=2026-01-01" , string (body ))
1035+
1036+ // Request 4: HIT — same as request 3
1037+ resp = makeReq ("en-us" , "2026-01-01" )
1038+ testutil .AssertEqual (t , http .StatusOK , resp .StatusCode )
1039+ testutil .AssertEqual (
1040+ t ,
1041+ internal .CacheStatusHit .Value ,
1042+ resp .Header .Get (internal .CacheStatusHeader ),
1043+ )
1044+ body , _ = io .ReadAll (resp .Body )
1045+ _ = resp .Body .Close ()
1046+ testutil .AssertEqual (t , "lang=en-us date=2026-01-01" , string (body ))
1047+ }
0 commit comments