blob: 6b38a7c613c1312518058b522bcb3e30273d9191 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/http_stream_pool_attempt.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <ranges>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/memory/raw_ref.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "base/trace_event/trace_id_helper.h"
#include "base/types/expected.h"
#include "net/base/completion_once_callback.h"
#include "net/base/ip_endpoint.h"
#include "net/base/load_timing_info.h"
#include "net/base/net_errors.h"
#include "net/base/net_export.h"
#include "net/dns/host_resolver.h"
#include "net/dns/public/host_resolver_results.h"
#include "net/http/http_basic_stream.h"
#include "net/http/http_network_session.h"
#include "net/http/http_stream_key.h"
#include "net/http/http_stream_pool.h"
#include "net/log/net_log_with_source.h"
#include "net/socket/stream_attempt.h"
#include "net/socket/tcp_stream_attempt.h"
#include "net/socket/tls_stream_attempt.h"
#include "net/ssl/ssl_config.h"
#include "net/ssl/ssl_info.h"
namespace net {
// A single inner TCP-based attempt.
// TODO(crbug.com/457478038): Figure out better name for this class. Too many
// "Attempt" classes.
class HttpStreamPool::Attempt::TcpAttempt : public TlsStreamAttempt::Delegate {
public:
TcpAttempt(Attempt& owner, IPEndPoint ip_endpoint)
: owner_(owner), ip_endpoint_(std::move(ip_endpoint)) {
if (owner_->using_tls_) {
attempt_ = std::make_unique<TlsStreamAttempt>(
&owner_->stream_attempt_params_.get(), ip_endpoint_, owner_->track_,
HostPortPair::FromSchemeHostPort(
delegate().GetHttpStreamKey().destination()),
delegate().GetBaseSSLConfig(),
/*delegate=*/this);
} else {
attempt_ = std::make_unique<TcpStreamAttempt>(
&owner_->stream_attempt_params_.get(), ip_endpoint_, owner_->track_);
}
}
TcpAttempt(const TcpAttempt&) = delete;
TcpAttempt& operator=(const TcpAttempt&) = delete;
~TcpAttempt() override = default;
const IPEndPoint& ip_endpoint() const { return ip_endpoint_; }
StreamAttempt& stream_attempt() const { return *attempt_.get(); }
int Start(CompletionOnceCallback callback) {
start_time_ = base::TimeTicks::Now();
int rv = attempt_->Start(std::move(callback));
if (rv == ERR_IO_PENDING && !owner_->observed_slow_attempt_) {
slow_timer_.Start(FROM_HERE, HttpStreamPool::GetConnectionAttemptDelay(),
base::BindOnce(&TcpAttempt::OnSlowTimerFired,
base::Unretained(this)));
}
return rv;
}
// Called when the delegate's service endpoint request is updated or finished
// and endpoints are ready to start the TLS handshake. Note that this method
// can be called multiple times, even after the TLS handshake has already
// started.
void MaybeStartTlsHandshake() {
DCHECK(delegate().GetServiceEndpointRequest().EndpointsCryptoReady());
if (!tls_handshake_ready_callback_) {
return;
}
// Resume the slow timer if it was stopped.
CHECK(!slow_timer_.IsRunning());
if (!owner_->observed_slow_attempt_) {
CHECK(!is_slow_);
CHECK_GE(tcp_handshake_complete_time_, start_time_);
// Timer is not guaranteed to fire exactly on time, so we need to check
// if we are already past the delay.
base::TimeDelta tcp_handshake_duration =
tcp_handshake_complete_time_ - start_time_;
if (HttpStreamPool::GetConnectionAttemptDelay() >
tcp_handshake_duration) {
slow_timer_.Start(FROM_HERE,
HttpStreamPool::GetConnectionAttemptDelay() -
tcp_handshake_duration,
base::BindOnce(&TcpAttempt::OnSlowTimerFired,
base::Unretained(this)));
} else {
OnSlowTimerFired();
}
}
std::move(tls_handshake_ready_callback_).Run(OK);
}
// TlsStreamAttempt::Delegate implementation.
void OnTcpHandshakeComplete() override {
tcp_handshake_complete_time_ = base::TimeTicks::Now();
}
// Called from TlsStreamAttempt when it is ready to start the TLS handshake
// (i.e., TCP handshake is complete). If the endpoint is ready to start TLS
// handshake, returns OK. Otherwise, returns ERR_IO_PENDING and stores the
// callback to be called when the endpoint is ready to start TLS handshake.
int WaitForTlsHandshakeReady(CompletionOnceCallback callback) override {
if (owner_->is_crypto_ready_) {
return OK;
}
// Pause the slow timer while waiting for the endpoints to be ready for TLS
// handshake.
if (slow_timer_.IsRunning()) {
slow_timer_.Stop();
}
tls_handshake_ready_callback_ = std::move(callback);
return ERR_IO_PENDING;
}
// Called from TlsStreamAttempt to get the service endpoint for TLS
// handshake. Always called after the endpoint is ready to start TLS
// handshake.
base::expected<ServiceEndpoint, TlsStreamAttempt::GetServiceEndpointError>
GetServiceEndpointForTlsHandshake() override {
CHECK(delegate().GetServiceEndpointRequest().EndpointsCryptoReady());
return owner_->GetServiceEndpointForTlsHandshake(ip_endpoint_);
}
private:
Attempt::Delegate& delegate() const { return owner_->delegate_.get(); }
void OnSlowTimerFired() {
is_slow_ = true;
owner_->OnTcpAttemptSlow(this);
}
const raw_ref<Attempt> owner_;
const IPEndPoint ip_endpoint_;
base::OneShotTimer slow_timer_;
bool is_slow_ = false;
base::TimeTicks start_time_;
base::TimeTicks tcp_handshake_complete_time_;
std::unique_ptr<StreamAttempt> attempt_;
CompletionOnceCallback tls_handshake_ready_callback_;
};
HttpStreamPool::Attempt::Attempt(
Delegate& delegate,
const StreamAttemptParams& stream_attempt_params,
NetLogWithSource net_log)
: delegate_(delegate),
stream_attempt_params_(stream_attempt_params),
using_tls_(GURL::SchemeIsCryptographic(
delegate_->GetHttpStreamKey().destination().scheme())),
track_(base::trace_event::GetNextGlobalTraceId()),
net_log_(std::move(net_log)) {
if (delegate_->GetServiceEndpointRequest().EndpointsCryptoReady()) {
is_crypto_ready_ = true;
}
}
HttpStreamPool::Attempt::~Attempt() = default;
void HttpStreamPool::Attempt::Start() {
MaybeAttempt();
}
void HttpStreamPool::Attempt::ProcessServiceEndpointChanges() {
// First, trigger TLS handshakes for existing attempts if endpoints are ready
// to start TLS handshakes.
HostResolver::ServiceEndpointRequest& service_endpoint_request =
delegate_->GetServiceEndpointRequest();
if (!is_crypto_ready_ && service_endpoint_request.EndpointsCryptoReady()) {
is_crypto_ready_ = true;
// Starting TLS handshake may fail synchronously (also may succeed
// synchronously, unlikely to happen in reality though). Synchronous
// completion of an attempt could end up deleting `this`. Use WeakPtr to
// detect whether `this` is deleted. Alternatively, we can use PostTask to
// schedule `this` to be deleted when an attempt succeeds or fails. However,
// scheduling tasks does have some overhead so we use WeakPtr here.
base::WeakPtr<Attempt> weak_self = weak_ptr_factory_.GetWeakPtr();
for (auto& attempt : {ipv6_attempt_.get(), ipv4_attempt_.get()}) {
if (!attempt) {
continue;
}
attempt->MaybeStartTlsHandshake();
if (!weak_self) {
// `this` is deleted. Have to return immediately.
return;
}
}
}
// Second, try to start new attempts if possible.
MaybeAttempt();
}
void HttpStreamPool::Attempt::MaybeAttempt() {
std::optional<IPEndPoint> ip_endpoint = GetIPEndPointToAttempt();
if (!ip_endpoint.has_value()) {
// If all endpoints are failed, notify the delegate. Otherwise, wait for
// further endpoints update, and/or completing in-flight attempts.
const bool all_endpoints_failed =
!ipv4_attempt_ && !ipv6_attempt_ &&
delegate_->IsServiceEndpointRequestFinished();
if (all_endpoints_failed) {
base::WeakPtr<Attempt> weak_this = weak_ptr_factory_.GetWeakPtr();
int error = most_recent_attempt_failure_.has_value()
? most_recent_attempt_failure_->error
: ERR_NAME_NOT_RESOLVED;
delegate_->OnAttemptFailure(this, error);
// Do not add code, `this` is deleted.
CHECK(!weak_this);
}
return;
}
StartAttempt(std::move(*ip_endpoint));
// `this` may be deleted at this point. For example, if the attempt fails
// synchronously and it's the last attempt.
}
void HttpStreamPool::Attempt::StartAttempt(IPEndPoint ip_endpoint) {
const bool is_ipv4 = ip_endpoint.address().IsIPv4();
DCHECK(is_ipv4 || ip_endpoint.address().IsIPv6());
auto& attempt = is_ipv4 ? ipv4_attempt_ : ipv6_attempt_;
CHECK(!attempt);
auto [_, inserted] = attempted_endpoints_.insert(ip_endpoint);
CHECK(inserted);
attempt = std::make_unique<TcpAttempt>(*this, std::move(ip_endpoint));
TcpAttempt* raw_attempt = attempt.get();
net_log_.AddEvent(
NetLogEventType::HTTP_STREAM_POOL_TCP_BASED_ATTEMPT_START, [&] {
base::DictValue dict;
dict.Set("ip_endpoint", raw_attempt->ip_endpoint().ToString());
raw_attempt->stream_attempt().net_log().source().AddToEventParameters(
dict);
return dict;
});
raw_attempt->stream_attempt().net_log().AddEventReferencingSource(
NetLogEventType::TCP_BASED_ATTEMPT_BOUND_TO_POOL, net_log_.source());
int rv = attempt->Start(base::BindOnce(&Attempt::OnTcpAttemptComplete,
weak_ptr_factory_.GetWeakPtr(),
raw_attempt));
if (rv != ERR_IO_PENDING) {
OnTcpAttemptComplete(raw_attempt, rv);
}
}
bool HttpStreamPool::Attempt::IsEndpointUsable(const ServiceEndpoint& endpoint,
bool svcb_optional) const {
// No ALPNs means that the endpoint is an authority A/AAAA endpoint, even if
// we are still in the middle of DNS resolution.
if (endpoint.metadata.supported_protocol_alpns.empty()) {
return svcb_optional;
}
// See https://www.rfc-editor.org/rfc/rfc9460.html#section-9.3. Endpoints are
// usable if there is an overlap between the endpoint's ALPNs and the
// configured ones.
return std::ranges::any_of(
endpoint.metadata.supported_protocol_alpns, [&](const auto& alpn) {
NextProto next_proto = NextProtoFromString(alpn);
if (!kTcpBasedProtocols.Has(next_proto)) {
return false;
}
return std::ranges::contains(delegate_->GetAlpnProtos(), next_proto);
});
}
std::optional<IPEndPoint> HttpStreamPool::Attempt::GetIPEndPointToAttempt()
const {
// Don't attempt more when the first attempt already exists and it's not slow.
const bool has_attempt = ipv4_attempt_ || ipv6_attempt_;
if (has_attempt && !observed_slow_attempt_) {
return std::nullopt;
}
HostResolver::ServiceEndpointRequest& service_endpoint_request =
delegate_->GetServiceEndpointRequest();
if (service_endpoint_request.GetEndpointResults().empty()) {
return std::nullopt;
}
// If there is a previous failed attempt, prefer the other address family.
// Otherwise prefer IPv6.
const bool prefer_ipv6 =
!most_recent_attempt_failure_ ||
!most_recent_attempt_failure_->ip_endpoint.address().IsIPv6();
const bool svcb_optional = delegate_->IsSvcbOptional();
const base::span<const ServiceEndpoint>& endpoint_results =
service_endpoint_request.GetEndpointResults();
for (bool use_ipv6 : {prefer_ipv6, !prefer_ipv6}) {
if ((use_ipv6 && ipv6_attempt_) || (!use_ipv6 && ipv4_attempt_)) {
continue;
}
for (const auto& service_endpoint : endpoint_results) {
if (!IsEndpointUsable(service_endpoint, svcb_optional)) {
continue;
}
const std::vector<IPEndPoint>& ip_endpoints =
use_ipv6 ? service_endpoint.ipv6_endpoints
: service_endpoint.ipv4_endpoints;
for (const auto& ip_endpoint : ip_endpoints) {
if (attempted_endpoints_.contains(ip_endpoint)) {
continue;
}
return ip_endpoint;
}
}
}
return std::nullopt;
}
base::expected<ServiceEndpoint, TlsStreamAttempt::GetServiceEndpointError>
HttpStreamPool::Attempt::GetServiceEndpointForTlsHandshake(
const IPEndPoint& ip_endpoint) const {
HostResolver::ServiceEndpointRequest& service_endpoint_request =
delegate_->GetServiceEndpointRequest();
const bool svcb_optional = delegate_->IsSvcbOptional();
for (const auto& service_endpoint :
service_endpoint_request.GetEndpointResults()) {
if (!IsEndpointUsable(service_endpoint, svcb_optional)) {
continue;
}
const std::vector<IPEndPoint>& ip_endpoints =
ip_endpoint.address().IsIPv4() ? service_endpoint.ipv4_endpoints
: service_endpoint.ipv6_endpoints;
if (!std::ranges::contains(ip_endpoints, ip_endpoint)) {
continue;
}
return service_endpoint;
}
return base::unexpected(TlsStreamAttempt::GetServiceEndpointError::kAbort);
}
void HttpStreamPool::Attempt::OnTcpAttemptSlow(TcpAttempt* attempt) {
CHECK(attempt == ipv4_attempt_.get() || attempt == ipv6_attempt_.get());
net_log_.AddEvent(
NetLogEventType::HTTP_STREAM_POOL_ATTEMPT_MANAGER_TCP_BASED_ATTEMPT_SLOW,
[&] {
base::DictValue dict;
dict.Set("ip_endpoint", attempt->ip_endpoint().ToString());
attempt->stream_attempt().net_log().source().AddToEventParameters(dict);
return dict;
});
observed_slow_attempt_ = true;
MaybeAttempt();
}
void HttpStreamPool::Attempt::OnTcpAttemptComplete(TcpAttempt* attempt,
int rv) {
net_log_.AddEvent(
NetLogEventType::HTTP_STREAM_POOL_TCP_BASED_ATTEMPT_END, [&] {
base::DictValue dict;
dict.Set("ip_endpoint", attempt->ip_endpoint().ToString());
dict.Set("net_error", rv);
attempt->stream_attempt().net_log().source().AddToEventParameters(dict);
return dict;
});
std::unique_ptr<TcpAttempt> completed_attempt;
if (attempt->ip_endpoint().address().IsIPv4()) {
completed_attempt = std::move(ipv4_attempt_);
} else {
completed_attempt = std::move(ipv6_attempt_);
}
CHECK_EQ(attempt, completed_attempt.get());
if (rv != OK) {
HandleSingleFailure(std::move(completed_attempt), rv);
// `this` may be deleted at this point.
return;
}
std::unique_ptr<StreamSocket> stream_socket =
completed_attempt->stream_attempt().ReleaseStreamSocket();
CHECK(stream_socket);
LoadTimingInfo::ConnectTiming connect_timing =
completed_attempt->stream_attempt().connect_timing();
HostResolver::ServiceEndpointRequest& service_endpoint_request =
delegate_->GetServiceEndpointRequest();
stream_socket->SetDnsAliases(service_endpoint_request.GetDnsAliasResults());
// Reset `completed_attempt` before notifying delegate to prevent dangling
// pointer.
completed_attempt.reset();
base::WeakPtr<Attempt> weak_this = weak_ptr_factory_.GetWeakPtr();
delegate_->OnStreamSocketReady(this, std::move(stream_socket),
std::move(connect_timing));
// `this` is deleted.
CHECK(!weak_this);
}
void HttpStreamPool::Attempt::HandleSingleFailure(
std::unique_ptr<TcpAttempt> attempt,
int rv) {
CHECK_NE(rv, OK);
CHECK_NE(rv, ERR_IO_PENDING);
most_recent_attempt_failure_ = {rv, attempt->ip_endpoint()};
// If the error is fatal, i.e., if the attempt requires a client certificate
// or a certificate error, we should notify the delegate immediately instead
// of trying further endpoints.
if (rv == ERR_SSL_CLIENT_AUTH_CERT_NEEDED) {
scoped_refptr<SSLCertRequestInfo> ssl_cert_request_info =
attempt->stream_attempt().GetCertRequestInfo();
CHECK(ssl_cert_request_info);
attempt.reset();
base::WeakPtr<Attempt> weak_this = weak_ptr_factory_.GetWeakPtr();
delegate_->OnNeedsClientCertificate(this, std::move(ssl_cert_request_info));
// `this` is deleted.
CHECK(!weak_this);
return;
}
if (IsCertificateError(rv)) {
CHECK(attempt->stream_attempt().stream_socket());
SSLInfo ssl_info;
bool has_ssl_info =
attempt->stream_attempt().stream_socket()->GetSSLInfo(&ssl_info);
CHECK(has_ssl_info);
attempt.reset();
base::WeakPtr<Attempt> weak_this = weak_ptr_factory_.GetWeakPtr();
delegate_->OnCertificateError(this, rv, ssl_info);
// `this` is deleted.
CHECK(!weak_this);
return;
}
attempt.reset();
MaybeAttempt();
// Be careful of `this` may be deleted at this point.
}
} // namespace net