@@ -8,6 +8,7 @@ use std::{fs::File, io::IsTerminal, time::Duration};
88use backon:: { ExponentialBuilder , Retryable } ;
99use futures_util:: StreamExt ;
1010use indicatif:: { ProgressBar , ProgressStyle } ;
11+ use serde:: de:: DeserializeOwned ;
1112use sha2:: { Digest , Sha256 } ;
1213use tokio:: { fs, io:: AsyncWriteExt } ;
1314use vite_path:: { AbsolutePath , AbsolutePathBuf } ;
@@ -16,10 +17,9 @@ use vite_str::Str;
1617use crate :: { Error , provider:: ArchiveFormat } ;
1718
1819/// Response from a cached fetch operation
19- pub struct CachedFetchResponse {
20- /// Response body (None if 304 Not Modified)
21- #[ expect( clippy:: disallowed_types, reason = "HTTP response body is a String" ) ]
22- pub body : Option < String > ,
20+ pub struct CachedFetchResponse < T > {
21+ /// Deserialized response body (None if 304 Not Modified)
22+ pub body : Option < T > ,
2323 /// `ETag` header value
2424 pub etag : Option < Str > ,
2525 /// Cache max-age in seconds (from Cache-Control header)
@@ -164,26 +164,60 @@ pub async fn download_text(url: &str) -> Result<String, Error> {
164164 Ok ( content)
165165}
166166
167- /// Fetch text with conditional request support
167+ /// Fetch JSON with conditional request support.
168168///
169169/// If `if_none_match` is provided, sends `If-None-Match` header for conditional request.
170- /// Returns response with cache headers and `not_modified` flag.
171- pub async fn fetch_with_cache_headers (
170+ /// The request, response body, and JSON decoding are retried as one operation so a
171+ /// truncated body cannot escape the retry boundary as a deserialization error.
172+ pub async fn fetch_json_with_cache_headers < T : DeserializeOwned > (
172173 url : & str ,
173174 if_none_match : Option < & str > ,
174- ) -> Result < CachedFetchResponse , Error > {
175+ ) -> Result < CachedFetchResponse < T > , Error > {
175176 let client = vite_shared:: shared_http_client ( ) ;
176177
177178 tracing:: debug!( "Fetching with cache headers from {url}" ) ;
178179
179- let response = ( || async {
180+ ( || async {
180181 let mut request = client. get ( url) ;
181182
182183 if let Some ( etag) = if_none_match {
183184 request = request. header ( "If-None-Match" , etag) ;
184185 }
185186
186- request. send ( ) . await
187+ let response = request. send ( ) . await ?. error_for_status ( ) ?;
188+
189+ if response. status ( ) == reqwest:: StatusCode :: NOT_MODIFIED {
190+ tracing:: debug!( "Received 304 Not Modified for {url}" ) ;
191+ return Ok ( CachedFetchResponse {
192+ body : None ,
193+ etag : None ,
194+ max_age : None ,
195+ not_modified : true ,
196+ } ) ;
197+ }
198+
199+ // Extract headers before consuming the response.
200+ let etag = response
201+ . headers ( )
202+ . get ( "etag" )
203+ . and_then ( |v| v. to_str ( ) . ok ( ) )
204+ . map ( std:: convert:: Into :: into) ;
205+
206+ let max_age = response
207+ . headers ( )
208+ . get ( "cache-control" )
209+ . and_then ( |v| v. to_str ( ) . ok ( ) )
210+ . and_then ( parse_max_age) ;
211+
212+ let bytes = response. bytes ( ) . await ?;
213+ let body = serde_json:: from_slice ( & bytes) ?;
214+
215+ Ok :: < CachedFetchResponse < T > , Error > ( CachedFetchResponse {
216+ body : Some ( body) ,
217+ etag,
218+ max_age,
219+ not_modified : false ,
220+ } )
187221 } )
188222 . retry (
189223 ExponentialBuilder :: default ( )
@@ -195,35 +229,7 @@ pub async fn fetch_with_cache_headers(
195229 . map_err ( |e| Error :: DownloadFailed {
196230 url : url. into ( ) ,
197231 reason : vite_shared:: format_error_chain ( & e) . into ( ) ,
198- } ) ?;
199-
200- // Check for 304 Not Modified
201- if response. status ( ) == reqwest:: StatusCode :: NOT_MODIFIED {
202- tracing:: debug!( "Received 304 Not Modified for {url}" ) ;
203- return Ok ( CachedFetchResponse {
204- body : None ,
205- etag : None ,
206- max_age : None ,
207- not_modified : true ,
208- } ) ;
209- }
210-
211- // Extract headers before consuming response
212- let etag =
213- response. headers ( ) . get ( "etag" ) . and_then ( |v| v. to_str ( ) . ok ( ) ) . map ( std:: convert:: Into :: into) ;
214-
215- let max_age = response
216- . headers ( )
217- . get ( "cache-control" )
218- . and_then ( |v| v. to_str ( ) . ok ( ) )
219- . and_then ( parse_max_age) ;
220-
221- let body = response. text ( ) . await . map_err ( |e| Error :: DownloadFailed {
222- url : url. into ( ) ,
223- reason : vite_shared:: format_error_chain ( & e) . into ( ) ,
224- } ) ?;
225-
226- Ok ( CachedFetchResponse { body : Some ( body) , etag, max_age, not_modified : false } )
232+ } )
227233}
228234
229235/// Parse max-age from Cache-Control header value
0 commit comments