Skip to content

Commit ffd620c

Browse files
committed
fix(client): use MEDIA_CLIENT with redirect following for media proxy
1 parent 97ede02 commit ffd620c

1 file changed

Lines changed: 40 additions & 4 deletions

File tree

src/client.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
3131

3232
pub 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+
3463
pub 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

200234
async 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

Comments
 (0)