avoid reusing non-persistent connections during 302 redirects (#4690)
Some checks failed
Android / build (push) Has been cancelled
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Docker / build (push) Has been cancelled
Linux / build (push) Has been cancelled
Linux_Python / build (push) Has been cancelled
macOS / build (push) Has been cancelled
macOS_Python / build (push) Has been cancelled
Windows / build (push) Has been cancelled
Windows_Python / build (push) Has been cancelled

When performing a 302 redirect, if the server explicitly indicates that the connection is not a persistent connection, the current
connection should not be reused; this prevents potential failures caused
by certain servers actively closing the connection.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
YuLi 2026-03-22 19:40:55 -07:00 committed by GitHub
parent 899d9653a4
commit 4daa30e229
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 38 deletions

View File

@ -22,6 +22,8 @@ HlsPlayer::HlsPlayer(const EventPoller::Ptr &poller) {
void HlsPlayer::play(const string &url) {
_play_result = false;
_play_url = url;
_last_sequence = -1;
_playlist_reload_changed = true;
setProxyUrl((*this)[Client::kProxyUrl]);
setAllowResendRequest(true);
fetchIndexFile();
@ -202,10 +204,12 @@ void HlsPlayer::fetchSegment() {
bool HlsPlayer::onParsed(bool is_m3u8_inner, int64_t sequence, const map<int, ts_segment> &ts_map) {
if (!is_m3u8_inner) {
auto playlist_changed = _last_sequence != sequence;
_playlist_reload_changed = playlist_changed;
// 这是ts播放列表 [AUTO-TRANSLATED:7ce3d81b]
// This is the ts playlist
// This is the ts playlist
if (_last_sequence == sequence) {
if (!playlist_changed) {
// 如果是重复的ts列表那么忽略 [AUTO-TRANSLATED:d15a47f3]
// If it is a duplicate ts list, then ignore it
// 但是需要注意, 如果当前ts列表为空了, 那么表明直播结束了或者m3u8文件有问题,需要重新拉流 [AUTO-TRANSLATED:438a8df0]
@ -312,27 +316,23 @@ float HlsPlayer::delaySecond() {
if (HlsParser::isM3u8() && HlsParser::getTargetDur() > 0) {
float targetOffset;
if (HlsParser::isLive()) {
// see RFC 8216, Section 4.4.3.8.
// 根据rfc刷新index列表的周期应该是分段时间x3, 因为根据规范播放器只处理最后3个Segment [AUTO-TRANSLATED:07168708]
// According to the rfc, the refresh cycle of the index list should be 3 times the segment time, because according to the specification, the player only processes the last 3 Segments
// refresh the index list according to rfc cycle should be the segment time x3,
// because according to the specification, the player only handles the last 3 segments
targetOffset = (float)(3 * HlsParser::getTargetDur());
// RFC 8216 Section 6.3.4:
// after a changed playlist reload, wait at least one target
// duration; after an unchanged reload, wait half a target duration.
return _playlist_reload_changed ? (float) HlsParser::getTargetDur()
: (float) HlsParser::getTargetDur() / 2.0f;
} else {
// 点播则一般m3u8文件不会在改变了, 没必要频繁的刷新, 所以按照总时间来进行刷新 [AUTO-TRANSLATED:2ac0a29e]
// On-demand generally does not change the m3u8 file, there is no need to refresh frequently, so refresh according to the total time
// On-demand, the m3u8 file will generally not change, so there is no need to refresh frequently,
targetOffset = HlsParser::getTotalDuration();
}
// 取最小值, 避免因为分段时长不规则而导致的问题 [AUTO-TRANSLATED:073dff48]
// Take the minimum value to avoid problems caused by irregular segment durations
// Take the minimum value to avoid problems caused by irregular segment duration
if (targetOffset > HlsParser::getTotalDuration()) {
targetOffset = HlsParser::getTotalDuration();
}
// 根据规范为一半的时间 [AUTO-TRANSLATED:07652637]
// According to the specification, it is half the time
// According to the specification, it is half the time
if (targetOffset / 2 > 1.0f) {
return targetOffset / 2;
}

View File

@ -130,6 +130,10 @@ private:
int _timeout_multiple = MIN_TIMEOUT_MULTIPLE;
int _try_fetch_index_times = 0;
int _ts_download_failed_count = 0;
// RFC 8216 reload interval depends on whether the last media sequence
// changed. We intentionally keep this lightweight and only track the
// sequence number for live playlist refresh timing.
bool _playlist_reload_changed = true;
protected:
size_t _recvtotalbytes = 0;

View File

@ -18,6 +18,15 @@ using namespace toolkit;
namespace mediakit {
static bool connectionContainsClose(const string &connection) {
for (auto token : split(connection, ",")) {
if (strToLower(trim(std::move(token))) == "close") {
return true;
}
}
return false;
}
void HttpClient::sendRequest(const string &url) {
clearResponse();
_url = url;
@ -56,26 +65,24 @@ void HttpClient::sendRequest(const string &url) {
}
auto host_header = host;
splitUrl(host, host, port);
auto keep_alive = _request_keep_alive;
auto persistent = _http_persistent && keep_alive;
bool protocol_changed = (_is_https != is_https);
bool host_changed = (_last_host != host + ":" + to_string(port)) || protocol_changed;
_last_host = host + ":" + to_string(port);
_is_https = is_https;
_header.emplace("Host", host_header);
_header.emplace("User-Agent", kServerName);
_header.emplace("Accept", "*/*");
_header.emplace("Accept-Language", "zh-CN,zh;q=0.8");
if (_http_persistent) {
_header.emplace("Connection", "keep-alive");
} else {
_header.emplace("Connection", "close");
}
_http_persistent = true;
_header.emplace("Connection", keep_alive ? "keep-alive" : "close");
if (_body && _body->remainSize()) {
_header.emplace("Content-Length", to_string(_body->remainSize()));
GET_CONFIG(string, charSet, Http::kCharSet);
_header.emplace("Content-Type", "application/x-www-form-urlencoded; charset=" + charSet);
}
bool host_changed = (_last_host != host + ":" + to_string(port)) || (_is_https != is_https);
_last_host = host + ":" + to_string(port);
_is_https = is_https;
auto cookies = HttpCookieStorage::Instance().get(_last_host, _path);
_StrPrinter printer;
for (auto &cookie : cookies) {
@ -85,13 +92,24 @@ void HttpClient::sendRequest(const string &url) {
printer.pop_back();
_header.emplace("Cookie", printer);
}
if (!alive() || host_changed || !_http_persistent) {
if (isUsedProxy()) {
if (isUsedProxy()) {
// All proxy traffic uses CONNECT, so reuse is limited to the same tunnel target.
bool proxy_reuse = alive() && persistent && !host_changed && _proxy_connected;
if (!proxy_reuse) {
_http_persistent = keep_alive;
_proxy_connected = false;
startConnect(_proxy_host, _proxy_port, _wait_header_ms / 1000.0f);
startConnectWithProxy(host, _proxy_host, _proxy_port, _wait_header_ms / 1000.0f);
} else {
startConnect(host, port, _wait_header_ms / 1000.0f);
SockException ex;
onConnect_l(ex);
}
return;
}
if (!alive() || host_changed || !persistent) {
_http_persistent = keep_alive;
startConnect(host, port, _wait_header_ms / 1000.0f);
} else {
SockException ex;
onConnect_l(ex);
@ -103,8 +121,9 @@ void HttpClient::clear() {
_user_set_header.clear();
_body.reset();
_method.clear();
// 重置代理连接状态
_proxy_connected = false;
_request_keep_alive = true;
// Keep transport-level state so a live direct/proxy connection can still
// be reused after the caller resets only the per-request state.
clearResponse();
}
@ -202,9 +221,8 @@ void HttpClient::onRecv(const Buffer::Ptr &pBuf) {
void HttpClient::onError(const SockException &ex) {
if (ex.getErrCode() == Err_reset && _allow_resend_request && _http_persistent && _recved_body_size == 0 && !_header_recved) {
// 连接被重置,可能是服务器主动断开了连接, 或者服务器内核参数或防火墙的持久连接空闲时间超时或不一致. [AUTO-TRANSLATED:8a78f452]
// The connection was reset, possibly because the server actively closed the connection, or the server kernel parameters or firewall's persistent connection idle timeout or inconsistency.
// 如果是持久化连接,那么我们可以通过重连来解决这个问题 [AUTO-TRANSLATED:6c113e17]
// 连接被重置,可能是服务器主动断开了连接, 或者服务器内核参数或防火墙的持久连接空闲时间超时或不一致.
// 如果是持久化连接,那么我们可以通过重连来解决这个问题
// If it is a persistent connection, we can solve this problem by reconnecting
// The connection was reset, possibly because the server actively disconnected the connection,
// or the persistent connection idle time of the server kernel parameters or firewall timed out or inconsistent.
@ -219,12 +237,17 @@ void HttpClient::onError(const SockException &ex) {
ssize_t HttpClient::onRecvHeader(const char *data, size_t len) {
_parser.parse(data, len);
if (_parser.status() == "302" || _parser.status() == "301" || _parser.status() == "303") {
auto connection_close = connectionContainsClose(_parser["Connection"]);
if (connection_close) {
_http_persistent = false;
}
if (_parser.status() == "302" || _parser.status() == "301" || _parser.status() == "303" || _parser.status() == "307") {
auto new_url = Parser::mergeUrl(_url, _parser["Location"]);
if (new_url.empty()) {
throw invalid_argument("未找到Location字段(跳转url)");
}
if (onRedirectUrl(new_url, _parser.status() == "302")) {
bool temporary_redirect = _parser.status() == "302" || _parser.status() == "307";
if (onRedirectUrl(new_url, temporary_redirect)) {
HttpClient::sendRequest(new_url);
return 0;
}
@ -467,6 +490,13 @@ void HttpClient::setCompleteTimeout(size_t timeout_ms) {
_wait_complete_ms = timeout_ms;
}
void HttpClient::setRequestKeepAlive(bool enable) {
_request_keep_alive = enable;
if (!enable) {
_http_persistent = false;
}
}
bool HttpClient::isUsedProxy() const {
return _used_proxy;
}
@ -476,12 +506,34 @@ bool HttpClient::isProxyConnected() const {
}
void HttpClient::setProxyUrl(string proxy_url) {
auto old_used_proxy = _used_proxy;
auto old_proxy_host = _proxy_host;
auto old_proxy_port = _proxy_port;
auto old_proxy_auth = _proxy_auth;
_proxy_url = std::move(proxy_url);
if (!_proxy_url.empty()) {
_proxy_host.clear();
_proxy_port = 0;
_proxy_auth.clear();
parseProxyUrl(_proxy_url, _proxy_host, _proxy_port, _proxy_auth);
_used_proxy = true;
} else {
_used_proxy = false;
_proxy_host.clear();
_proxy_port = 0;
_proxy_auth.clear();
_proxy_connected = false;
}
auto proxy_config_changed = old_used_proxy != _used_proxy
|| old_proxy_host != _proxy_host
|| old_proxy_port != _proxy_port
|| old_proxy_auth != _proxy_auth;
if (proxy_config_changed) {
// A proxy mode or endpoint change must not reuse the previous transport.
_http_persistent = false;
_proxy_connected = false;
}
}
@ -493,6 +545,10 @@ bool HttpClient::checkProxyConnected(const char *data, size_t len) {
}
_proxy_connected = false;
// CONNECT failed, which usually means the proxy rejected the tunnel request,
// does not support CONNECT for this target, or the proxy authentication is invalid.
WarnL << "proxy CONNECT failed, status line: "
<< response.substr(0, response.find("\r\n"));
return false;
}

View File

@ -60,8 +60,8 @@ public:
virtual void sendRequest(const std::string &url);
/**
*
* Reset object
*
* Reset per-request state while preserving reusable transport state when possible
* [AUTO-TRANSLATED:d23b5bbb]
*/
@ -168,10 +168,10 @@ public:
void setHeaderTimeout(size_t timeout_ms);
/**
* body数据超时时间, 5
* body数据超时时间, 10
* body回复的超时问题
* 0
* Set the timeout for receiving body data, default 5 seconds
* Set the timeout for receiving body data, default 10 seconds
* This parameter can be used to handle timeout issues for large body responses
* This parameter can be equal to 0
@ -189,6 +189,14 @@ public:
*/
void setCompleteTimeout(size_t timeout_ms);
/**
* keep-alive
* Set whether requests should advertise keep-alive semantics, enabled by default
* [AUTO-TRANSLATED:6f62f63c]
*/
void setRequestKeepAlive(bool enable);
/**
* http代理url
* Set http proxy url
@ -276,7 +284,6 @@ private:
void onResponseCompleted_l(const toolkit::SockException &ex);
void onConnect_l(const toolkit::SockException &ex);
void checkCookie(HttpHeader &headers);
private:
//for http response
bool _complete = false;
@ -289,7 +296,7 @@ private:
std::shared_ptr<HttpChunkedSplitter> _chunked_splitter;
//for request args
bool _is_https;
bool _is_https = false;
std::string _url;
HttpHeader _user_set_header;
HttpBody::Ptr _body;
@ -308,9 +315,10 @@ private:
toolkit::Ticker _wait_body;
toolkit::Ticker _wait_complete;
bool _request_keep_alive = true;
bool _used_proxy = false;
bool _proxy_connected = false;
uint16_t _proxy_port;
uint16_t _proxy_port = 0;
std::string _proxy_url;
std::string _proxy_host;
std::string _proxy_auth;