@@ -102,7 +102,7 @@ impl Fetcher for YouTubeFetcher {
102102 let mut client_builder = reqwest:: Client :: builder ( )
103103 . connect_timeout ( API_TIMEOUT )
104104 . timeout ( API_TIMEOUT )
105- . redirect ( reqwest:: redirect:: Policy :: limited ( 3 ) ) ;
105+ . redirect ( reqwest:: redirect:: Policy :: none ( ) ) ;
106106
107107 if !options. respect_proxy_env {
108108 client_builder = client_builder. no_proxy ( ) ;
@@ -120,6 +120,7 @@ impl Fetcher for YouTubeFetcher {
120120 // Fetch oEmbed metadata
121121 // The canonical URL only contains safe ASCII chars, so it can be passed directly
122122 let mut oembed_url = Url :: parse ( "https://www.youtube.com/oembed" ) . unwrap ( ) ;
123+ options. validate_url ( & oembed_url) ?;
123124 oembed_url
124125 . query_pairs_mut ( )
125126 . append_pair ( "url" , & canonical_url)
@@ -144,8 +145,7 @@ impl Fetcher for YouTubeFetcher {
144145 let author_url = oembed. as_ref ( ) . and_then ( |o| o. author_url . clone ( ) ) ;
145146
146147 // Attempt transcript extraction via timedtext API
147- let transcript =
148- fetch_transcript ( & client, & ua_header, & video_id, options. max_body_size ) . await ;
148+ let transcript = fetch_transcript ( & client, & ua_header, & video_id, options) . await ;
149149
150150 let content = format_youtube_response (
151151 & title,
@@ -173,16 +173,19 @@ async fn fetch_transcript(
173173 client : & reqwest:: Client ,
174174 ua : & HeaderValue ,
175175 video_id : & str ,
176- max_body_size : Option < usize > ,
176+ options : & FetchOptions ,
177177) -> Option < String > {
178178 // Try the legacy timedtext API (auto-generated English captions)
179179 let timedtext_url = format ! (
180180 "https://www.youtube.com/api/timedtext?v={}&lang=en&fmt=srv3" ,
181181 video_id
182182 ) ;
183183
184+ let timedtext_url = Url :: parse ( & timedtext_url) . ok ( ) ?;
185+ options. validate_url ( & timedtext_url) . ok ( ) ?;
186+
184187 let resp = client
185- . get ( & timedtext_url)
188+ . get ( timedtext_url. as_str ( ) )
186189 . header ( USER_AGENT , ua. clone ( ) )
187190 . send ( )
188191 . await
@@ -193,7 +196,7 @@ async fn fetch_transcript(
193196 }
194197
195198 let xml = resp. text ( ) . await . ok ( ) ?;
196- if let Some ( max_body_size) = max_body_size {
199+ if let Some ( max_body_size) = options . max_body_size {
197200 if xml. len ( ) > max_body_size {
198201 return None ;
199202 }
@@ -475,6 +478,19 @@ mod tests {
475478 assert ! ( segments. is_empty( ) ) ;
476479 }
477480
481+ #[ tokio:: test]
482+ async fn test_fetch_blocked_secondary_host ( ) {
483+ let fetcher = YouTubeFetcher :: new ( ) ;
484+ let request = FetchRequest :: new ( "https://youtu.be/dQw4w9WgXcQ" ) ;
485+ let options = FetchOptions {
486+ blocked_hosts : vec ! [ ".youtube.com" . to_string( ) ] ,
487+ ..Default :: default ( )
488+ } ;
489+
490+ let result = fetcher. fetch ( & request, & options) . await ;
491+ assert ! ( matches!( result, Err ( FetchError :: BlockedUrl ) ) ) ;
492+ }
493+
478494 #[ test]
479495 fn test_decode_xml_entities ( ) {
480496 assert_eq ! ( decode_xml_entities( "a & b" ) , "a & b" ) ;
0 commit comments