11package transport
22
33import (
4+ "bytes"
45 "context"
56 "io"
67 "net/http"
@@ -14,6 +15,27 @@ import (
1415 "github.com/stretchr/testify/require"
1516)
1617
18+ type roundTripFunc func (* http.Request ) (* http.Response , error )
19+
20+ func (f roundTripFunc ) RoundTrip (req * http.Request ) (* http.Response , error ) {
21+ return f (req )
22+ }
23+
24+ type countingReadCloser struct {
25+ reader io.Reader
26+ bytesRead int
27+ }
28+
29+ func (c * countingReadCloser ) Read (p []byte ) (int , error ) {
30+ n , err := c .reader .Read (p )
31+ c .bytesRead += n
32+ return n , err
33+ }
34+
35+ func (c * countingReadCloser ) Close () error {
36+ return nil
37+ }
38+
1739// TestETagTransport_ServesCachedBodyOn304 verifies the core conditional-request
1840// flow: the first GET carries no If-None-Match and is cached with its ETag; the
1941// second GET sends the cached ETag and, on a 304 Not Modified, is served the
@@ -236,6 +258,43 @@ func TestETagTransport_SkipsBodiesOverEntryByteBudget(t *testing.T) {
236258 assert .Empty (t , lastIfNoneMatch , "an oversized response must not be cached or revalidated" )
237259}
238260
261+ func TestETagTransport_StreamsOversizedUnknownLengthBody (t * testing.T ) {
262+ t .Parallel ()
263+
264+ const maxEntry = 16
265+ body := []byte ("0123456789abcdef-streamed-remainder" )
266+ stream := & countingReadCloser {reader : bytes .NewReader (body )}
267+ transport := roundTripFunc (func (req * http.Request ) (* http.Response , error ) {
268+ assert .Empty (t , req .Header .Get (headers .IfNoneMatchHeader ), "an oversized response must not be cached or revalidated" )
269+ return & http.Response {
270+ StatusCode : http .StatusOK ,
271+ Status : "200 OK" ,
272+ Header : http.Header {headers .ETagHeader : []string {`"streamed"` }},
273+ Body : stream ,
274+ ContentLength : - 1 ,
275+ Request : req ,
276+ }, nil
277+ })
278+ rt := & ETagTransport {Transport : transport , MaxEntryBytes : maxEntry }
279+
280+ req , err := http .NewRequestWithContext (context .Background (), http .MethodGet , "https://example.test/stream" , nil )
281+ require .NoError (t , err )
282+ resp , err := rt .RoundTrip (req )
283+ require .NoError (t , err )
284+ require .LessOrEqual (t , stream .bytesRead , maxEntry + 1 , "RoundTrip must not read the entire oversized body before returning" )
285+
286+ data , err := io .ReadAll (resp .Body )
287+ require .NoError (t , err )
288+ require .NoError (t , resp .Body .Close ())
289+ assert .Equal (t , body , data , "caller receives the buffered prefix plus the streamed remainder" )
290+
291+ stream = & countingReadCloser {reader : bytes .NewReader (body )}
292+ resp , err = rt .RoundTrip (req )
293+ require .NoError (t , err )
294+ _ , _ = io .Copy (io .Discard , resp .Body )
295+ require .NoError (t , resp .Body .Close ())
296+ }
297+
239298// TestETagTransport_EvictsByTotalByteBudget verifies that inserting a second
240299// entry that pushes the cache over its total-byte budget evicts the
241300// least-recently-used entry, which is then re-fetched in full.
0 commit comments