@@ -31,6 +31,35 @@ const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
3131
3232pub static CLIENT : LazyLock < WreqClient > = LazyLock :: new ( build_client) ;
3333
34+ /// A separate wreq client used only for media proxying (images, videos, HLS).
35+ /// Unlike CLIENT (which uses Policy::none() so the Reddit API redirect logic
36+ /// in request() stays intact), this client follows redirects automatically so
37+ /// that CDN 302s are resolved server-side and the browser always receives the
38+ /// final media bytes rather than a redirect to a bare Reddit CDN URL.
39+ pub static MEDIA_CLIENT : LazyLock < WreqClient > = LazyLock :: new ( || {
40+ let emulations = [
41+ Emulation :: Chrome135 ,
42+ Emulation :: Chrome136 ,
43+ Emulation :: Chrome137 ,
44+ Emulation :: Firefox135 ,
45+ Emulation :: Firefox136 ,
46+ ] ;
47+ let emulation_os = [ EmulationOS :: Android , EmulationOS :: Windows , EmulationOS :: Linux ] ;
48+
49+ let rand = fastrand:: usize ( ..) ;
50+ let emulation = EmulationOption :: builder ( )
51+ . emulation ( emulations[ rand % emulations. len ( ) ] )
52+ . emulation_os ( emulation_os[ rand % emulation_os. len ( ) ] )
53+ . build ( )
54+ . emulation ( ) ;
55+
56+ WreqClient :: builder ( )
57+ . emulation ( emulation)
58+ . redirect ( Policy :: limited ( 10 ) )
59+ . build ( )
60+ . expect ( "Should always be able to build a wreq media client" )
61+ } ) ;
62+
3463pub static OAUTH_CLIENT : LazyLock < ArcSwap < Oauth > > = LazyLock :: new ( || {
3564 let client = block_on ( Oauth :: new ( ) ) ;
3665 tokio:: spawn ( token_daemon ( ) ) ;
@@ -97,9 +126,14 @@ impl IntoHyperResponse for wreq::Response {
97126
98127 let mut builder = hyper:: Response :: builder ( ) . status ( status) ;
99128 for ( key_bytes, val_bytes) in & raw_headers {
100- let key = hyper:: header:: HeaderName :: from_bytes ( key_bytes) . map_err ( |e| e. to_string ( ) ) ?;
101- let val = hyper:: header:: HeaderValue :: from_bytes ( val_bytes) . map_err ( |e| e. to_string ( ) ) ?;
102- builder = builder. header ( key, val) ;
129+ // Skip headers that hyper rejects (e.g. non-ASCII bytes in CDN headers)
130+ // rather than aborting the entire response.
131+ if let ( Ok ( key) , Ok ( val) ) = (
132+ hyper:: header:: HeaderName :: from_bytes ( key_bytes) ,
133+ hyper:: header:: HeaderValue :: from_bytes ( val_bytes) ,
134+ ) {
135+ builder = builder. header ( key, val) ;
136+ }
103137 }
104138 // wreq already decompresses; remove the content-encoding header so
105139 // downstream code doesn't try to decompress again.
@@ -198,7 +232,9 @@ pub async fn proxy(req: hyper::Request<Body>, format: &str) -> Result<Response<B
198232}
199233
200234async fn stream ( url : & str , req : & hyper:: Request < Body > ) -> Result < Response < Body > , String > {
201- let mut builder = CLIENT . get ( url) ;
235+ // Use MEDIA_CLIENT (Policy::limited(10)) so CDN 302 redirects are followed
236+ // server-side. CLIENT uses Policy::none() for Reddit API redirect handling.
237+ let mut builder = MEDIA_CLIENT . get ( url) ;
202238
203239 // Copy useful headers from original request
204240 // Convert hyper header values (http v0.2) to bytes for wreq (http v1.x)
0 commit comments