Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// gtests for HappyEyeballsConnectionAttempt. Two injected seams exercise the
// dispatch logic without real sockets: a fake ConnectionEstablisherFactory
// (establishers complete on demand) and a recording
// HappyEyeballsConnMgrDelegate (intercepts connection-manager/entry calls). The
// real Rust engine stays live, driven via its FFI inputs; an IP-literal origin
// avoids DNS.
#include "gtest/gtest.h"
#include "ConnectionEntry.h"
#include "ConnectionEstablisher.h"
#include "HappyEyeballsConnMgrDelegate.h"
#include "HappyEyeballsConnectionAttempt.h"
#include "HappyEyeballsTransaction.h"
#include "HttpConnectionBase.h"
#include "HttpTrafficAnalyzer.h"
#include "NullHttpTransaction.h"
#include "PendingTransactionInfo.h"
#include "ZeroRttHandle.h"
#include "mozilla/Preferences.h"
#include "mozilla/net/ClassOfService.h"
#include "mozilla/net/DNS.h"
#include "nsHttpConnectionInfo.h"
#include "nsHttpRequestHead.h"
#include "nsHttpTransaction.h"
#include "nsISeekableStream.h"
#include "nsISocketTransportService.h"
#include "nsIHttpProtocolHandler.h"
#include "nsNetAddr.h"
#include "nsServiceManagerUtils.h"
#include "nsSocketTransportService2.h"
#include "nsThreadUtils.h"
namespace mozilla {
namespace net {
namespace {
// Minimal HttpConnectionBase: trivial pure-virtual bodies plus a few knobs
// (UsingSpdy/UsingHttp3, Activate result) the success path reads.
class FakeHttpConnection final : public HttpConnectionBase {
public:
NS_DECL_THREADSAFE_ISUPPORTS
explicit FakeHttpConnection(bool aUsingHttp3, bool aUsingSpdy)
: mUsingHttp3(aUsingHttp3), mUsingSpdy(aUsingSpdy) {}
bool UsingSpdy() override { return mUsingSpdy; }
bool UsingHttp3() override { return mUsingHttp3; }
[[nodiscard]] nsresult Activate(nsAHttpTransaction*, uint32_t,
int32_t) override {
return NS_OK;
}
void Close(nsresult, bool) override {}
bool CanReuse() override { return false; }
bool CanDirectlyActivate() override { return false; }
void DontReuse() override {}
nsAHttpTransaction* Transaction() override { return nullptr; }
void CloseTransaction(nsAHttpTransaction*, nsresult, bool) override {}
[[nodiscard]] nsresult OnHeadersAvailable(nsAHttpTransaction*,
nsHttpRequestHead*,
nsHttpResponseHead*,
bool*) override {
return NS_OK;
}
[[nodiscard]] nsresult TakeTransport(nsISocketTransport**,
nsIAsyncInputStream**,
nsIAsyncOutputStream**) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
void PrintDiagnostics(nsCString&) override {}
bool TestJoinConnection(const nsACString&, int32_t) override { return false; }
bool JoinConnection(const nsACString&, int32_t) override { return false; }
void GetTLSSocketControl(nsITLSSocketControl** aResult) override {
*aResult = nullptr;
}
[[nodiscard]] nsresult ResumeSend() override { return NS_OK; }
[[nodiscard]] nsresult ResumeRecv() override { return NS_OK; }
[[nodiscard]] nsresult ForceSend() override { return NS_OK; }
[[nodiscard]] nsresult ForceRecv() override { return NS_OK; }
HttpVersion Version() override {
return mUsingHttp3 ? HttpVersion::v3_0
: (mUsingSpdy ? HttpVersion::v2_0 : HttpVersion::v1_1);
}
bool LastTransactionExpectedNoContent() override { return false; }
void SetLastTransactionExpectedNoContent(bool) override {}
int64_t BytesWritten() override { return 0; }
bool IsPersistent() override { return true; }
bool IsReused() override { return false; }
[[nodiscard]] nsresult PushBack(const char*, uint32_t) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
void SetEvent(nsresult) override {}
nsresult GetSelfAddr(NetAddr* aAddr) override {
*aAddr = NetAddr();
return NS_OK;
}
nsresult GetPeerAddr(NetAddr* aAddr) override {
*aAddr = NetAddr();
return NS_OK;
}
bool ResolvedByTRR() override { return false; }
nsIRequest::TRRMode EffectiveTRRMode() override {
return nsIRequest::TRR_DEFAULT_MODE;
}
TRRSkippedReason TRRSkipReason() override {
return nsITRRSkipReason::TRR_UNSET;
}
bool GetEchConfigUsed() override { return false; }
PRIntervalTime LastWriteTime() override { return 0; }
[[nodiscard]] nsresult CreateTunnelStream(nsAHttpTransaction*,
HttpConnectionBase**,
bool) override {
return NS_ERROR_NOT_IMPLEMENTED;
}
private:
~FakeHttpConnection() = default;
bool mUsingHttp3;
bool mUsingSpdy;
};
NS_IMPL_ISUPPORTS(FakeHttpConnection, nsISupportsWeakReference)
// Establisher that captures the completion callback instead of opening a
// socket. The test fires the result whenever it wants.
class FakeConnectionEstablisher final : public ConnectionEstablisher {
public:
NS_INLINE_DECL_REFCOUNTING_INHERITED(FakeConnectionEstablisher,
ConnectionEstablisher)
FakeConnectionEstablisher(nsHttpConnectionInfo* aConnInfo,
const NetAddr& aAddr, uint32_t aCaps, bool aIsUDP)
: ConnectionEstablisher(aConnInfo, aAddr, aCaps), mIsUDP(aIsUDP) {}
bool Start(DoneCallback&& aCallback) override {
mCallback = std::move(aCallback);
return true;
}
void Close(nsresult) override { ++mCloseCount; }
void ResetSpeculativeFlags() override {}
bool IsUDP() const override { return mIsUDP; }
void FireSuccess(HttpConnectionBase* aConn) {
mResultConn = aConn;
mCallback(RefPtr<HttpConnectionBase>(aConn));
}
void FireError(nsresult aError) { mCallback(Err(aError)); }
uint32_t mCloseCount = 0;
private:
~FakeConnectionEstablisher() = default;
void Finish(nsresult) override {}
bool mIsUDP;
};
class FakeConnectionEstablisherFactory final
: public ConnectionEstablisherFactory {
public:
already_AddRefed<ConnectionEstablisher> Create(
ConnectionEstablisherType aType, nsHttpConnectionInfo* aConnInfo,
const NetAddr& aAddr, uint32_t aCaps, bool, bool) override {
bool isUDP = aType == ConnectionEstablisherType::UDP;
RefPtr<FakeConnectionEstablisher> e =
new FakeConnectionEstablisher(aConnInfo, aAddr, aCaps, isUDP);
(isUDP ? mUDP : mTCP).AppendElement(e);
return e.forget();
}
nsTArray<RefPtr<FakeConnectionEstablisher>> mTCP;
nsTArray<RefPtr<FakeConnectionEstablisher>> mUDP;
};
class RecordingConnMgrDelegate final : public HappyEyeballsConnMgrDelegate {
public:
already_AddRefed<PendingTransactionInfo> FindTransaction(
bool, ConnectionEntry*, nsAHttpTransaction*) override {
mCalls.AppendElement("FindTransaction"_ns);
return do_AddRef(mFindResult);
}
nsresult DispatchTransaction(ConnectionEntry*, nsHttpTransaction* aTrans,
HttpConnectionBase* aConn) override {
mCalls.AppendElement("DispatchTransaction"_ns);
// Like the real ConnMgr, bind the transaction to the connection. Keep the
// handle so the test can Reset it before teardown (~ConnectionHandle would
// otherwise reclaim the fake connection and crash).
if (mSimulateDispatchBindsConnection && aTrans && aConn) {
RefPtr<ConnectionHandle> handle = new ConnectionHandle(aConn);
aTrans->SetConnection(handle);
mDispatchHandles.AppendElement(handle);
}
return mDispatchRv;
}
void AddTransaction(nsHttpTransaction*, int32_t) override {
mCalls.AppendElement("AddTransaction"_ns);
}
void ReportSpdyConnection(HttpConnectionBase*, bool, bool) override {
mCalls.AppendElement("ReportSpdyConnection"_ns);
}
void ReportHttp3Connection(HttpConnectionBase*, ConnectionEntry*) override {
mCalls.AppendElement("ReportHttp3Connection"_ns);
}
void ReclaimConnection(HttpConnectionBase*) override {
mCalls.AppendElement("ReclaimConnection"_ns);
}
void ProcessSpdyPendingQ(ConnectionEntry*) override {
mCalls.AppendElement("ProcessSpdyPendingQ"_ns);
}
void InsertIntoActiveConns(ConnectionEntry*, HttpConnectionBase*) override {
mCalls.AppendElement("InsertIntoActiveConns"_ns);
}
void RemoveConnectionAttempt(ConnectionEntry*, ConnectionAttempt*,
bool) override {
mCalls.AppendElement("RemoveConnectionAttempt"_ns);
}
void RecordIPFamilyPreference(ConnectionEntry*, uint16_t) override {
mCalls.AppendElement("RecordIPFamilyPreference"_ns);
}
void ResetIPFamilyPreference(ConnectionEntry*) override {
mCalls.AppendElement("ResetIPFamilyPreference"_ns);
}
bool MaybeProcessCoalescingKeys(ConnectionEntry*, const nsTArray<NetAddr>&,
bool) override {
mCalls.AppendElement("MaybeProcessCoalescingKeys"_ns);
return false;
}
bool RemoveTransFromPendingQ(ConnectionEntry*, nsHttpTransaction*) override {
mCalls.AppendElement("RemoveTransFromPendingQ"_ns);
return false;
}
nsresult StartRetryWithoutTRR(ConnectionEntry*, nsHttpTransaction*, uint32_t,
bool, bool, bool) override {
mCalls.AppendElement("StartRetryWithoutTRR"_ns);
return mStartRetryWithoutTRRRv;
}
int32_t Count(const char* aName) const {
int32_t n = 0;
for (const auto& s : mCalls) {
if (s.EqualsASCII(aName)) {
n++;
}
}
return n;
}
// Clear the connection handles created by DispatchTransaction so their
// destructors don't reclaim the fake connections.
void ResetHandles() {
for (auto& handle : mDispatchHandles) {
handle->Reset();
}
}
nsTArray<nsCString> mCalls;
RefPtr<PendingTransactionInfo> mFindResult;
nsresult mDispatchRv = NS_OK;
nsresult mStartRetryWithoutTRRRv = NS_OK;
bool mSimulateDispatchBindsConnection = false;
nsTArray<RefPtr<ConnectionHandle>> mDispatchHandles;
};
struct TestHarness {
RefPtr<nsHttpConnectionInfo> mConnInfo;
RefPtr<nsAHttpTransaction> mTrans;
nsTHashSet<ConnectionEntry*> mPendingQSet;
RefPtr<ConnectionEntry> mEntry;
RefPtr<FakeConnectionEstablisherFactory> mFactory;
RefPtr<RecordingConnMgrDelegate> mDelegate;
RefPtr<HappyEyeballsConnectionAttempt> mHE;
// aRealTransaction: use a real nsHttpTransaction (QueryHttpTransaction()
// non-null) for paths that touch the real request; else a
// NullHttpTransaction.
explicit TestHarness(uint32_t aCaps = 0, bool aRealTransaction = false) {
mConnInfo = new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns,
nullptr, OriginAttributes(),
/*endToEndSSL*/ true);
if (aRealTransaction) {
mTrans = new nsHttpTransaction();
} else {
mTrans = new NullHttpTransaction(mConnInfo, nullptr, aCaps);
}
mEntry = new ConnectionEntry(mConnInfo, mPendingQSet);
mFactory = new FakeConnectionEstablisherFactory();
mDelegate = new RecordingConnMgrDelegate();
mHE = new HappyEyeballsConnectionAttempt(mConnInfo, mTrans, aCaps,
/*speculative*/ false,
/*urgentStart*/ false);
mHE->SetConnectionEstablisherFactoryForTesting(mFactory);
mHE->SetConnMgrDelegateForTesting(mDelegate);
}
void Init() { mHE->Init(mEntry); }
};
// Ensure gHttpHandler is initialized (the HCA ctor notifies HTTP activity
// through it). Must run on the main thread.
static void EnsureHttpHandler() {
nsCOMPtr<nsIHttpProtocolHandler> http =
do_GetService("@mozilla.org/network/protocol;1?name=http");
ASSERT_TRUE(http);
}
// Run `aFn` on the socket thread and block until it completes.
static void RunOnSocketThread(std::function<void()>&& aFn) {
nsCOMPtr<nsIEventTarget> sts = gSocketTransportService;
ASSERT_TRUE(sts);
NS_DispatchAndSpinEventLoopUntilComplete(
"TestHappyEyeballs"_ns, sts,
NS_NewRunnableFunction("TestHappyEyeballs", std::move(aFn)));
}
} // namespace
// Init() with an IP-literal origin should drive the engine to attempt a
// connection through the injected factory rather than opening a real socket.
TEST(HappyEyeballsConnectionAttempt, FactorySeamUsed)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
TestHarness h;
h.Init();
EXPECT_GE(h.mFactory->mTCP.Length(), 1u)
<< "engine should have requested a TCP establisher via the factory";
EXPECT_EQ(h.mFactory->mUDP.Length(), 0u);
h.mHE->Abandon();
});
}
// Firing a successful TCP connection runs Succeeded -> EnterSucceeded ->
// ProcessTCPConn, which dispatches the pending real transaction onto the
// winning connection through the delegate.
TEST(HappyEyeballsConnectionAttempt, TcpSuccessRunsDispatchViaDelegate)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
TestHarness h(/*caps*/ 0, /*realTransaction*/ true);
nsHttpTransaction* realTrans = h.mTrans->QueryHttpTransaction();
ASSERT_TRUE(realTrans);
// The pending-queue lookup returns the real transaction so ProcessTCPConn
// dispatches it rather than reclaiming the connection.
h.mDelegate->mFindResult = new PendingTransactionInfo(realTrans);
h.Init();
ASSERT_GE(h.mFactory->mTCP.Length(), 1u);
RefPtr<FakeHttpConnection> conn =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false);
h.mFactory->mTCP[0]->FireSuccess(conn);
EXPECT_TRUE(h.mHE->IsTerminal());
EXPECT_EQ(h.mDelegate->Count("RecordIPFamilyPreference"), 1);
EXPECT_EQ(h.mDelegate->Count("InsertIntoActiveConns"), 1);
EXPECT_EQ(h.mDelegate->Count("FindTransaction"), 1);
EXPECT_EQ(h.mDelegate->Count("DispatchTransaction"), 1)
<< "the pending transaction should be dispatched onto the connection";
EXPECT_TRUE(h.mHE->WasTransactionAdoptedForTesting());
EXPECT_EQ(h.mDelegate->Count("ReportSpdyConnection"), 1);
});
}
// A failing connection result (the only address) drives the engine to Failed
// and EnterFailed, which removes the attempt and never dispatches.
TEST(HappyEyeballsConnectionAttempt, TcpFailureDoesNotDispatch)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
TestHarness h;
h.Init();
ASSERT_GE(h.mFactory->mTCP.Length(), 1u);
h.mFactory->mTCP[0]->FireError(NS_ERROR_CONNECTION_REFUSED);
EXPECT_TRUE(h.mHE->IsTerminal());
EXPECT_EQ(h.mDelegate->Count("DispatchTransaction"), 0);
EXPECT_EQ(h.mDelegate->Count("InsertIntoActiveConns"), 0);
EXPECT_GE(h.mDelegate->Count("RemoveConnectionAttempt"), 1);
});
}
// 0-RTT started but no winner, then a non-0-RTT conn wins. EnterSucceeded's
// fallback (AnyStarted && !HadWinner) must re-queue the real transaction once
// and not mark it adopted. Uses the ZeroRttHandle seam to set AnyStarted.
TEST(HappyEyeballsConnectionAttempt, ZeroRttNoWinnerFallback)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
TestHarness h(/*caps*/ 0, /*realTransaction*/ true);
h.Init();
ASSERT_GE(h.mFactory->mTCP.Length(), 1u);
// A racer entered 0-RTT but never became the winner.
h.mHE->ZeroRttHandleForTesting()->SetAnyStartedForTesting();
RefPtr<FakeHttpConnection> conn =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false);
h.mFactory->mTCP[0]->FireSuccess(conn);
EXPECT_TRUE(h.mHE->IsTerminal());
EXPECT_EQ(h.mDelegate->Count("AddTransaction"), 1)
<< "fallback should re-queue the real transaction once";
EXPECT_EQ(h.mDelegate->Count("DispatchTransaction"), 0);
EXPECT_FALSE(h.mHE->WasTransactionAdoptedForTesting());
});
}
// ReleaseRealTransaction guard (bug 2048392), real-world race: two HCAs race
// one transaction. The winner dispatches it onto its connection; the loser's
// attempt then fails and runs ReleaseRealTransaction. Since the transaction is
// already on the winner's connection, the loser must not close it or remove it
// from the pending queue.
TEST(HappyEyeballsConnectionAttempt,
ReleaseRealTransactionSkipsCloseWhenConnected)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr,
OriginAttributes(), /*endToEndSSL*/ true);
RefPtr<nsHttpTransaction> realTrans = new nsHttpTransaction();
nsTHashSet<ConnectionEntry*> pendingQSet;
// --- HCA #1: wins and dispatches the shared transaction. ---
RefPtr<ConnectionEntry> entry1 = new ConnectionEntry(ci, pendingQSet);
RefPtr<FakeConnectionEstablisherFactory> factory1 =
new FakeConnectionEstablisherFactory();
RefPtr<RecordingConnMgrDelegate> delegate1 = new RecordingConnMgrDelegate();
// Winner's dispatch binds the transaction to its connection.
delegate1->mSimulateDispatchBindsConnection = true;
delegate1->mFindResult = new PendingTransactionInfo(realTrans);
RefPtr<HappyEyeballsConnectionAttempt> hca1 =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
hca1->SetConnectionEstablisherFactoryForTesting(factory1);
hca1->SetConnMgrDelegateForTesting(delegate1);
hca1->Init(entry1);
ASSERT_GE(factory1->mTCP.Length(), 1u);
RefPtr<FakeHttpConnection> conn1 =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false);
factory1->mTCP[0]->FireSuccess(conn1);
ASSERT_TRUE(hca1->WasTransactionAdoptedForTesting());
ASSERT_NE(realTrans->Connection(), nullptr)
<< "the winning attempt should have bound the transaction to its conn";
// --- HCA #2: races the same transaction and fails. ---
RefPtr<ConnectionEntry> entry2 = new ConnectionEntry(ci, pendingQSet);
RefPtr<FakeConnectionEstablisherFactory> factory2 =
new FakeConnectionEstablisherFactory();
RefPtr<RecordingConnMgrDelegate> delegate2 = new RecordingConnMgrDelegate();
RefPtr<HappyEyeballsConnectionAttempt> hca2 =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
hca2->SetConnectionEstablisherFactoryForTesting(factory2);
hca2->SetConnMgrDelegateForTesting(delegate2);
hca2->Init(entry2);
ASSERT_GE(factory2->mTCP.Length(), 1u);
factory2->mTCP[0]->FireError(NS_ERROR_CONNECTION_REFUSED);
EXPECT_TRUE(hca2->IsTerminal());
EXPECT_FALSE(realTrans->Closed())
<< "the loser must not close a transaction already on the winner conn";
EXPECT_EQ(delegate2->Count("RemoveTransFromPendingQ"), 0)
<< "guard returns before touching the pending queue";
EXPECT_EQ(hca2->RealHttpTransaction(), nullptr)
<< "the loser drops its reference to the real transaction";
// Teardown: detach the connection and reset the handle the winner's
// dispatch created so ~ConnectionHandle doesn't reclaim the fake conn.
realTrans->SetConnection(nullptr);
delegate1->ResetHandles();
});
}
// ProcessTCPConn guard (bug 2048294), real-world race: a WebSocket/WebTransport
// upgrade is raced by two HCAs. The first wins and dispatches it (Connection()
// set). The second wins an H2 connection, whose ProcessTCPConn would normally
// re-queue the upgrade for the extended CONNECT tunnel; but since it's already
// on a connection it must NOT re-queue (re-dispatch would hit a transport-less
// connection and crash in Activate).
TEST(HappyEyeballsConnectionAttempt, ProcessTCPConnSkipsRequeueWhenConnected)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr,
OriginAttributes(), /*endToEndSSL*/ true);
RefPtr<nsHttpTransaction> realTrans = new nsHttpTransaction();
// Makes deferExtendedConnect fire on an H2 connection.
realTrans->SetIsForWebTransport(true);
nsTHashSet<ConnectionEntry*> pendingQSet;
// --- HCA #1: wins on H1 and dispatches the shared transaction. ---
RefPtr<ConnectionEntry> entry1 = new ConnectionEntry(ci, pendingQSet);
RefPtr<FakeConnectionEstablisherFactory> factory1 =
new FakeConnectionEstablisherFactory();
RefPtr<RecordingConnMgrDelegate> delegate1 = new RecordingConnMgrDelegate();
delegate1->mSimulateDispatchBindsConnection = true;
delegate1->mFindResult = new PendingTransactionInfo(realTrans);
RefPtr<HappyEyeballsConnectionAttempt> hca1 =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
hca1->SetConnectionEstablisherFactoryForTesting(factory1);
hca1->SetConnMgrDelegateForTesting(delegate1);
hca1->Init(entry1);
ASSERT_GE(factory1->mTCP.Length(), 1u);
RefPtr<FakeHttpConnection> conn1 =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ false);
factory1->mTCP[0]->FireSuccess(conn1);
ASSERT_NE(realTrans->Connection(), nullptr);
// --- HCA #2: wins on H2; deferExtendedConnect must skip the re-queue. ---
RefPtr<ConnectionEntry> entry2 = new ConnectionEntry(ci, pendingQSet);
RefPtr<FakeConnectionEstablisherFactory> factory2 =
new FakeConnectionEstablisherFactory();
RefPtr<RecordingConnMgrDelegate> delegate2 = new RecordingConnMgrDelegate();
RefPtr<HappyEyeballsConnectionAttempt> hca2 =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
hca2->SetConnectionEstablisherFactoryForTesting(factory2);
hca2->SetConnMgrDelegateForTesting(delegate2);
hca2->Init(entry2);
ASSERT_GE(factory2->mTCP.Length(), 1u);
RefPtr<FakeHttpConnection> conn2 =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ true);
factory2->mTCP[0]->FireSuccess(conn2);
EXPECT_TRUE(hca2->IsTerminal());
EXPECT_EQ(delegate2->Count("AddTransaction"), 0)
<< "must not re-queue a transaction already on a connection";
EXPECT_EQ(delegate2->Count("FindTransaction"), 0)
<< "guard returns before touching the pending queue";
EXPECT_FALSE(realTrans->Closed());
EXPECT_EQ(hca2->RealHttpTransaction(), nullptr)
<< "deferExtendedConnect clears the loser's transaction reference";
realTrans->SetConnection(nullptr);
delegate1->ResetHandles();
});
}
// Bug 2051415: on H2->H1 fallback the connection manager abandons the stale
// in-flight attempt. Verifies the property the fix relies on: Abandon() closes
// the establisher, so a late success can't fire and re-queue the transaction.
TEST(HappyEyeballsConnectionAttempt, AbandonClosesInFlightEstablisher)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr,
OriginAttributes(), /*endToEndSSL*/ true);
RefPtr<nsHttpTransaction> realTrans = new nsHttpTransaction();
// Stand-in for the WebSocket upgrade.
realTrans->SetIsForWebTransport(true);
nsTHashSet<ConnectionEntry*> pendingQSet;
RefPtr<ConnectionEntry> entry = new ConnectionEntry(ci, pendingQSet);
RefPtr<FakeConnectionEstablisherFactory> factory =
new FakeConnectionEstablisherFactory();
RefPtr<RecordingConnMgrDelegate> delegate = new RecordingConnMgrDelegate();
RefPtr<HappyEyeballsConnectionAttempt> hca =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
hca->SetConnectionEstablisherFactoryForTesting(factory);
hca->SetConnMgrDelegateForTesting(delegate);
hca->Init(entry);
ASSERT_GE(factory->mTCP.Length(), 1u);
EXPECT_FALSE(hca->IsTerminal());
EXPECT_EQ(factory->mTCP[0]->mCloseCount, 0u)
<< "the establisher is still in flight before abandonment";
// The connection manager abandons the stale attempt on H2->H1 fallback.
hca->Abandon();
EXPECT_TRUE(hca->IsTerminal())
<< "an abandoned attempt is terminal and cannot reach ProcessTCPConn";
EXPECT_EQ(factory->mTCP[0]->mCloseCount, 1u)
<< "abandoning closes the in-flight establisher, so a late success "
"can't fire and re-queue the restarted transaction (bug 2051415)";
EXPECT_FALSE(realTrans->Closed())
<< "abandoning the attempt must not close the restarted transaction";
});
}
// Bug 2051415, 0-RTT corner. The manager detaches the transaction
// (ForgetRealTransaction) before abandoning, so EnterDone's "0-RTT started, no
// winner" path can't re-queue it (cf. ZeroRttNoWinnerFallback, which does).
TEST(HappyEyeballsConnectionAttempt,
ForgetRealTransactionPreventsAbandonRequeue)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
TestHarness h(/*caps*/ 0, /*realTransaction*/ true);
h.Init();
ASSERT_GE(h.mFactory->mTCP.Length(), 1u);
// A racer entered 0-RTT but never won -- the case EnterDone re-queues.
h.mHE->ZeroRttHandleForTesting()->SetAnyStartedForTesting();
// Manager takes over the transaction, detaching it before abandoning.
h.mHE->ForgetRealTransaction();
h.mHE->Abandon();
EXPECT_TRUE(h.mHE->IsTerminal());
EXPECT_EQ(h.mDelegate->Count("AddTransaction"), 0)
<< "a detached, abandoned attempt must not re-queue the transaction "
"the connection manager is restarting (bug 2051415)";
EXPECT_EQ(h.mFactory->mTCP[0]->mCloseCount, 1u)
<< "abandoning still closes the in-flight establisher";
});
}
// 0-RTT winner-offset handling: several racers each advance the real
// transaction's request stream to a different Request0RttStreamOffset (the
// bytes that racer sent as early data). When one wins via Finish0RTT(accept),
// ZeroRttHandle must seek the real request stream to the *winner's* offset (so
// the real transaction resumes exactly where the winner left off) — not the
// loser's.
TEST(HappyEyeballsConnectionAttempt, ZeroRttWinnerUsesItsOwnStreamOffset)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr,
OriginAttributes(), /*endToEndSSL*/ true);
// The request head is held by weak reference inside the transaction, so it
// must outlive realTrans (declared first).
nsHttpRequestHead reqHead;
reqHead.SetMethod("GET"_ns);
reqHead.SetVersion(HttpVersion::v1_1);
reqHead.SetRequestURI("/"_ns);
RefPtr<nsHttpTransaction> realTrans = new nsHttpTransaction();
nsresult rv = realTrans->Init(
NS_HTTP_USE_HAPPY_EYEBALLS, ci, &reqHead, /*reqBody*/ nullptr,
/*reqContentLength*/ 0,
/*target*/ gSocketTransportService, /*callbacks*/ nullptr,
/*eventsink*/ nullptr, /*browserId*/ 0, HttpTrafficCategory::eInvalid,
/*requestContext*/ nullptr, ClassOfService(), /*initialRwin*/ 0,
/*responseTimeoutEnabled*/ false, /*channelId*/ 0,
/*transactionObserver*/ nullptr, nsILoadInfo::IPAddressSpace::Unknown,
LNAPerms{});
ASSERT_TRUE(NS_SUCCEEDED(rv));
// The serialized GET request is the (seekable) request stream; pick two
// distinct in-range offsets for the winning and losing racers.
nsCOMPtr<nsISeekableStream> seekable =
do_QueryInterface(realTrans->RequestStream());
ASSERT_TRUE(seekable);
ASSERT_TRUE(
NS_SUCCEEDED(seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0)));
uint64_t streamLen = 0;
ASSERT_TRUE(
NS_SUCCEEDED(realTrans->RequestStream()->Available(&streamLen)));
ASSERT_GE(streamLen, 4u);
uint64_t winnerOffset = streamLen / 4;
uint64_t loserOffset = streamLen / 2;
ASSERT_NE(winnerOffset, loserOffset);
RefPtr<HappyEyeballsConnectionAttempt> hca =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
ZeroRttHandle* zrh = hca->ZeroRttHandleForTesting();
ASSERT_TRUE(zrh);
auto noStatus = [](nsITransport*, nsresult, int64_t) {};
auto noAuth = []() {};
RefPtr<HappyEyeballsTransaction> hetLoser =
new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0,
noStatus, noAuth, noAuth, zrh);
RefPtr<HappyEyeballsTransaction> hetWinner =
new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0,
noStatus, noAuth, noAuth, zrh);
// Each racer sent a different amount of early data.
hetLoser->Request0RttStreamOffset() = Some(loserOffset);
hetWinner->Request0RttStreamOffset() = Some(winnerOffset);
// Finish0RTT -> AdoptWinner -> Adopt requires the winner to be on a
// connection; an H2 fake makes Adopt reuse our ConnectionHandle (the
// SwapTransaction is skipped because the fake isn't a real Http2Session).
RefPtr<FakeHttpConnection> winnerConn =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ true);
RefPtr<ConnectionHandle> winnerHandle = new ConnectionHandle(winnerConn);
hetWinner->SetConnection(winnerHandle);
hetWinner->SetConnectedCallback([](nsresult) {});
// Winner accepts 0-RTT (restart=false).
hetWinner->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false);
int64_t pos = -1;
ASSERT_TRUE(NS_SUCCEEDED(seekable->Tell(&pos)));
EXPECT_EQ(static_cast<uint64_t>(pos), winnerOffset)
<< "request stream must be seeked to the winner's 0-RTT offset, not "
"the "
"loser's";
// Teardown: detach + reset the handle so ~ConnectionHandle doesn't reclaim
// the fake connection.
realTrans->SetConnection(nullptr);
winnerHandle->Reset();
});
}
// Helper: a real, 0-RTT-capable transaction for the ZeroRttHandle tests.
static already_AddRefed<nsHttpTransaction> MakeRealTransaction(
nsHttpConnectionInfo* aCi, nsHttpRequestHead* aReqHead) {
aReqHead->SetMethod("GET"_ns);
aReqHead->SetVersion(HttpVersion::v1_1);
aReqHead->SetRequestURI("/"_ns);
RefPtr<nsHttpTransaction> trans = new nsHttpTransaction();
nsresult rv = trans->Init(
NS_HTTP_USE_HAPPY_EYEBALLS, aCi, aReqHead, /*reqBody*/ nullptr,
/*reqContentLength*/ 0, /*target*/ gSocketTransportService,
/*callbacks*/ nullptr,
/*eventsink*/ nullptr, /*browserId*/ 0, HttpTrafficCategory::eInvalid,
/*requestContext*/ nullptr, ClassOfService(), /*initialRwin*/ 0,
/*responseTimeoutEnabled*/ false, /*channelId*/ 0,
/*transactionObserver*/ nullptr, nsILoadInfo::IPAddressSpace::Unknown,
LNAPerms{});
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
return trans.forget();
}
// Regression for bug 2051776 (winner-then-loser). Once a winner is declared,
// ZeroRttHandle::Cleanup() nulls mWinner and leaves mState == CleanedUp, so a
// late Finish0RTT from a losing racer must be a no-op -- not trip the
// MOZ_ASSERT(mState == State::Open) in ZeroRttHandle::Finish0RTT.
TEST(HappyEyeballsConnectionAttempt, ZeroRttLateLoserFinish0RTTAfterWinner)
{
EnsureHttpHandler();
RunOnSocketThread([]() {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr,
OriginAttributes(), /*endToEndSSL*/ true);
// The request head is held by weak reference; it must outlive realTrans.
nsHttpRequestHead reqHead;
RefPtr<nsHttpTransaction> realTrans = MakeRealTransaction(ci, &reqHead);
RefPtr<HappyEyeballsConnectionAttempt> hca =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
ZeroRttHandle* zrh = hca->ZeroRttHandleForTesting();
ASSERT_TRUE(zrh);
auto noStatus = [](nsITransport*, nsresult, int64_t) {};
auto noAuth = []() {};
RefPtr<HappyEyeballsTransaction> hetWinner =
new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0,
noStatus, noAuth, noAuth, zrh);
RefPtr<HappyEyeballsTransaction> hetLoser =
new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0,
noStatus, noAuth, noAuth, zrh);
hetWinner->Request0RttStreamOffset() = Some<uint64_t>(0);
hetLoser->Request0RttStreamOffset() = Some<uint64_t>(0);
RefPtr<FakeHttpConnection> winnerConn =
new FakeHttpConnection(/*usingHttp3*/ false, /*usingSpdy*/ true);
RefPtr<ConnectionHandle> winnerHandle = new ConnectionHandle(winnerConn);
hetWinner->SetConnection(winnerHandle);
hetWinner->SetConnectedCallback([](nsresult) {});
// Declare the winner -> Cleanup() nulls mWinner, mState = CleanedUp.
hetWinner->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false);
// Late loser Finish0RTT after cleanup: must be a no-op (asserts before
// fix).
hetLoser->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false);
EXPECT_FALSE(realTrans->Closed());
realTrans->SetConnection(nullptr);
winnerHandle->Reset();
});
}
// Regression for bug 2051776 (real-txn-gone path). The first Finish0RTT takes
// the !realTxn branch and calls Cleanup() without ever declaring a winner
// (mHadWinner stays false), so a second Finish0RTT must still be a no-op. Only
// a state-based guard covers this path.
TEST(HappyEyeballsConnectionAttempt, ZeroRttLateLoserFinish0RTTAfterRealTxnGone)
{
EnsureHttpHandler();
Preferences::SetBool("network.http.0rtt_force_txn_gone_for_testing", true);
RunOnSocketThread([]() {
RefPtr<nsHttpConnectionInfo> ci =
new nsHttpConnectionInfo("127.0.0.1"_ns, 443, ""_ns, ""_ns, nullptr,
OriginAttributes(), /*endToEndSSL*/ true);
nsHttpRequestHead reqHead;
RefPtr<nsHttpTransaction> realTrans = MakeRealTransaction(ci, &reqHead);
RefPtr<HappyEyeballsConnectionAttempt> hca =
new HappyEyeballsConnectionAttempt(ci, realTrans, /*caps*/ 0,
/*speculative*/ false,
/*urgentStart*/ false);
ZeroRttHandle* zrh = hca->ZeroRttHandleForTesting();
ASSERT_TRUE(zrh);
auto noStatus = [](nsITransport*, nsresult, int64_t) {};
auto noAuth = []() {};
RefPtr<HappyEyeballsTransaction> het1 =
new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0,
noStatus, noAuth, noAuth, zrh);
RefPtr<HappyEyeballsTransaction> het2 =
new HappyEyeballsTransaction(ci, nullptr, /*caps*/ 0, /*browserId*/ 0,
noStatus, noAuth, noAuth, zrh);
het1->Request0RttStreamOffset() = Some<uint64_t>(0);
het2->Request0RttStreamOffset() = Some<uint64_t>(0);
// First Finish0RTT: the force pref makes the real txn "gone", taking the
// !realTxn branch -> Cleanup() without declaring a winner.
het1->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false);
// Second Finish0RTT after that cleanup: must be a no-op (asserts before
// fix).
het2->Finish0RTT(/*aRestart*/ false, /*aAlpnChanged*/ false);
realTrans->SetConnection(nullptr);
});
Preferences::ClearUser("network.http.0rtt_force_txn_gone_for_testing");
}
} // namespace net
} // namespace mozilla