@@ -172,6 +172,36 @@ impl HttpCache {
172172 self . fetch_and_store ( url) . await
173173 }
174174
175+ /// Fetches a URL with additional request headers, using the cache.
176+ ///
177+ /// Works the same as `get_cached` but injects extra headers (e.g., Authorization)
178+ /// into every request. Useful for APIs that require authentication tokens.
179+ pub async fn get_cached_with_headers (
180+ & self ,
181+ url : & str ,
182+ extra_headers : & [ ( header:: HeaderName , & str ) ] ,
183+ ) -> Result < Bytes > {
184+ if self . entries . len ( ) >= MAX_CACHE_ENTRIES {
185+ self . evict_entries ( ) ;
186+ }
187+
188+ if let Some ( cached) = self . entries . get ( url) . map ( |r| r. clone ( ) ) {
189+ match self
190+ . conditional_request_with_headers ( url, & cached, extra_headers)
191+ . await
192+ {
193+ Ok ( Some ( new_body) ) => return Ok ( new_body) ,
194+ Ok ( None ) => return Ok ( cached. body ) ,
195+ Err ( e) => {
196+ tracing:: warn!( "conditional request failed, using cache: {e}" ) ;
197+ return Ok ( cached. body ) ;
198+ }
199+ }
200+ }
201+
202+ self . fetch_and_store_with_headers ( url, extra_headers) . await
203+ }
204+
175205 /// Performs conditional HTTP request using cached validation headers.
176206 ///
177207 /// Sends `If-None-Match` (ETag) and/or `If-Modified-Since` headers
@@ -304,6 +334,124 @@ impl HttpCache {
304334 Ok ( body)
305335 }
306336
337+ async fn conditional_request_with_headers (
338+ & self ,
339+ url : & str ,
340+ cached : & CachedResponse ,
341+ extra_headers : & [ ( header:: HeaderName , & str ) ] ,
342+ ) -> Result < Option < Bytes > > {
343+ ensure_https ( url) ?;
344+ let mut request = self . client . get ( url) ;
345+
346+ for ( name, value) in extra_headers {
347+ request = request. header ( name, * value) ;
348+ }
349+ if let Some ( etag) = & cached. etag {
350+ request = request. header ( header:: IF_NONE_MATCH , etag) ;
351+ }
352+ if let Some ( last_modified) = & cached. last_modified {
353+ request = request. header ( header:: IF_MODIFIED_SINCE , last_modified) ;
354+ }
355+
356+ let response = request. send ( ) . await . map_err ( |e| DepsError :: RegistryError {
357+ package : url. to_string ( ) ,
358+ source : e,
359+ } ) ?;
360+
361+ if response. status ( ) == StatusCode :: NOT_MODIFIED {
362+ return Ok ( None ) ;
363+ }
364+
365+ if !response. status ( ) . is_success ( ) {
366+ let status = response. status ( ) ;
367+ return Err ( DepsError :: CacheError ( format ! ( "HTTP {status} for {url}" ) ) ) ;
368+ }
369+
370+ let etag = response
371+ . headers ( )
372+ . get ( header:: ETAG )
373+ . and_then ( |v| v. to_str ( ) . ok ( ) )
374+ . map ( String :: from) ;
375+ let last_modified = response
376+ . headers ( )
377+ . get ( header:: LAST_MODIFIED )
378+ . and_then ( |v| v. to_str ( ) . ok ( ) )
379+ . map ( String :: from) ;
380+ let body = response
381+ . bytes ( )
382+ . await
383+ . map_err ( |e| DepsError :: RegistryError {
384+ package : url. to_string ( ) ,
385+ source : e,
386+ } ) ?;
387+
388+ self . entries . insert (
389+ url. to_string ( ) ,
390+ CachedResponse {
391+ body : body. clone ( ) ,
392+ etag,
393+ last_modified,
394+ fetched_at : Instant :: now ( ) ,
395+ } ,
396+ ) ;
397+
398+ Ok ( Some ( body) )
399+ }
400+
401+ async fn fetch_and_store_with_headers (
402+ & self ,
403+ url : & str ,
404+ extra_headers : & [ ( header:: HeaderName , & str ) ] ,
405+ ) -> Result < Bytes > {
406+ ensure_https ( url) ?;
407+ tracing:: debug!( "fetching fresh with headers: {url}" ) ;
408+
409+ let mut request = self . client . get ( url) ;
410+ for ( name, value) in extra_headers {
411+ request = request. header ( name, * value) ;
412+ }
413+
414+ let response = request. send ( ) . await . map_err ( |e| DepsError :: RegistryError {
415+ package : url. to_string ( ) ,
416+ source : e,
417+ } ) ?;
418+
419+ if !response. status ( ) . is_success ( ) {
420+ let status = response. status ( ) ;
421+ return Err ( DepsError :: CacheError ( format ! ( "HTTP {status} for {url}" ) ) ) ;
422+ }
423+
424+ let etag = response
425+ . headers ( )
426+ . get ( header:: ETAG )
427+ . and_then ( |v| v. to_str ( ) . ok ( ) )
428+ . map ( String :: from) ;
429+ let last_modified = response
430+ . headers ( )
431+ . get ( header:: LAST_MODIFIED )
432+ . and_then ( |v| v. to_str ( ) . ok ( ) )
433+ . map ( String :: from) ;
434+ let body = response
435+ . bytes ( )
436+ . await
437+ . map_err ( |e| DepsError :: RegistryError {
438+ package : url. to_string ( ) ,
439+ source : e,
440+ } ) ?;
441+
442+ self . entries . insert (
443+ url. to_string ( ) ,
444+ CachedResponse {
445+ body : body. clone ( ) ,
446+ etag,
447+ last_modified,
448+ fetched_at : Instant :: now ( ) ,
449+ } ,
450+ ) ;
451+
452+ Ok ( body)
453+ }
454+
307455 /// Clears all cached entries.
308456 ///
309457 /// This removes all cached responses, forcing the next request for
0 commit comments