From ecddd5dd3c5d0c5209c34e193072704419a09cb8 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Thu, 16 Jul 2026 14:46:27 +0800 Subject: [PATCH] fix(http-proxy): preserve m3u redirect semantics --- e2e/test_http_proxy_m3u_rewrite.py | 30 ++++++++++++++++++++++++++++++ src/http_proxy.c | 9 ++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/e2e/test_http_proxy_m3u_rewrite.py b/e2e/test_http_proxy_m3u_rewrite.py index 69ef76a5..cac78c7c 100644 --- a/e2e/test_http_proxy_m3u_rewrite.py +++ b/e2e/test_http_proxy_m3u_rewrite.py @@ -688,6 +688,36 @@ def test_content_type_with_charset(self, shared_r2h): finally: upstream.stop() + def test_m3u_url_redirect_keeps_http_redirect_semantics(self, shared_r2h): + """A redirect from an M3U URL should rewrite Location, not its HTML body.""" + redirect_body = b"redirecting\n" + upstream = MockHTTPUpstream( + routes={ + "/archive/index.m3u8": { + "status": 302, + "body": redirect_body, + "headers": { + "Content-Type": "text/html", + "Location": "http://10.0.0.1:8080/final/index.m3u8", + }, + }, + } + ) + upstream.start() + try: + status, hdrs, body = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/archive/index.m3u8", + timeout=_TIMEOUT, + ) + + assert status == 302 + assert get_header(hdrs, "Location") == "/http/10.0.0.1:8080/final/index.m3u8" + assert body == redirect_body + finally: + upstream.stop() + # --------------------------------------------------------------------------- # Complex / realistic playlists diff --git a/src/http_proxy.c b/src/http_proxy.c index da15a070..a0eb5317 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -1133,14 +1133,17 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) { session->headers_received = 1; - /* Check if response body needs rewriting (M3U content). URL extension takes - * precedence; fall back to Content-Type only when the URL is not M3U-like. + /* Check if a successful response body needs rewriting (M3U content). URL + * extension takes precedence; fall back to Content-Type only when the URL + * is not M3U-like. Non-2xx responses must retain normal HTTP semantics, + * especially redirect Location rewriting and error response passthrough. * Skip for HEAD requests — there is no body to rewrite. */ int is_m3u_response = rewrite_is_m3u_url(session->target_path); if (!is_m3u_response) is_m3u_response = rewrite_is_m3u_content_type(session->response_content_type); - if (is_m3u_response && strcasecmp(session->method, "HEAD") != 0) { + if (session->response_status_code >= 200 && session->response_status_code < 300 && is_m3u_response && + strcasecmp(session->method, "HEAD") != 0) { if (session->transfer_encoding_seen && (!session->response_is_chunked || session->unsupported_transfer_coding)) { logger(LOG_ERROR, "HTTP Proxy: Unsupported Transfer-Encoding for M3U rewrite"); return -1;