net/quic: complete Fast Open response handling in OnIOComplete

With proxy Fast Open, Connect() can complete before the CONNECT
response arrives; the response then ran through OnIOComplete() into the
DoLoop() STATE_PROCESS_RESPONSE_CODE fastopen block, which force-returned
ERR_IO_PENDING. An async header-read failure left the consumed connect
callback to be run with an error (DCHECK in debug, null run in release),
and a pending application read was never completed.

Track the pending header read with std::optional<bool>
read_headers_pending_ (nullopt until Fast Open engages, false once the
read completes) and clear it in OnReadResponseHeadersComplete(). Remove
the STATE_PROCESS_RESPONSE_CODE block and handle the response result in
OnIOComplete(): on failure transition to STATE_DISCONNECTED and complete
a pending application read with the actual error; on success leave the
pending read to the data path.
pull/826/head
Lin 2026-09-09 20:27:32 +08:00
parent bfdd87ce08
commit 2e7fbdbb18
2 changed files with 9 additions and 57 deletions

View File

@ -47,7 +47,6 @@ QuicProxyClientSocket::QuicProxyClientSocket(
proxy_delegate_(proxy_delegate),
user_agent_(user_agent),
use_fastopen_(false),
read_headers_pending_(false),
net_log_(net_log) {
DCHECK(stream_->IsOpen());
@ -281,6 +280,13 @@ void QuicProxyClientSocket::OnIOComplete(int result) {
DCHECK_NE(STATE_DISCONNECTED, next_state_);
int rv = DoLoop(result);
if (rv != ERR_IO_PENDING) {
if (use_fastopen_ && read_headers_pending_ == false) {
if (rv != OK)
next_state_ = STATE_DISCONNECTED;
if (read_callback_ && rv != OK)
std::move(read_callback_).Run(rv);
return;
}
// Connect() finished (successfully or unsuccessfully).
DCHECK(!connect_callback_.is_null());
std::move(connect_callback_).Run(rv);
@ -326,23 +332,6 @@ int QuicProxyClientSocket::DoLoop(int last_io_result) {
rv = DoReadReplyComplete(rv);
net_log_.EndEventWithNetErrorCode(
NetLogEventType::HTTP_TRANSACTION_TUNNEL_READ_HEADERS, rv);
// If reading the response itself fails after Fast Open returned from
// Connect(), there is no connect_callback_ left to invoke. Successful
// responses continue through the normal response-processing states;
// HTTP status failures are handled in STATE_PROCESS_RESPONSE_CODE.
if (use_fastopen_ && read_headers_pending_ && rv < 0) {
read_headers_pending_ = false;
// The pending application read observes the closed stream. Any
// subsequent data after this response must be ignored.
next_state_ = STATE_DISCONNECTED;
// A Fast Open Connect() can complete before the app issues a
// Read(); a pending read must observe this failure instead of
// waiting for a stream close that may never come.
FailPendingReadOnFastOpenFailure(rv);
// The Fast Open Connect() already completed; do not report this
// response through connect_callback_.
rv = ERR_IO_PENDING;
}
break;
case STATE_PROCESS_RESPONSE_HEADERS:
DCHECK_EQ(OK, rv);
@ -354,21 +343,6 @@ int QuicProxyClientSocket::DoLoop(int last_io_result) {
case STATE_PROCESS_RESPONSE_CODE:
DCHECK_EQ(OK, rv);
rv = DoProcessResponseCode();
if (use_fastopen_ && read_headers_pending_) {
read_headers_pending_ = false;
if (rv < 0) {
// The pending application read observes the closed stream. Any
// subsequent data after this response must be ignored.
next_state_ = STATE_DISCONNECTED;
// A Fast Open Connect() can complete before the app issues a
// Read(); a pending read must observe this failure instead of
// waiting for a stream close that may never come.
FailPendingReadOnFastOpenFailure(rv);
}
// Fast Open already completed Connect(); do not invoke the
// consumed connect_callback_ for the later response.
rv = ERR_IO_PENDING;
}
break;
default:
NOTREACHED() << "bad state";
@ -602,6 +576,7 @@ int QuicProxyClientSocket::DoProcessResponseCode() {
void QuicProxyClientSocket::OnReadResponseHeadersComplete(int result) {
// Convert the now-populated quiche::HttpHeaderBlock to HttpResponseInfo
if (use_fastopen_ && read_headers_pending_) {
read_headers_pending_ = false;
if (next_state_ == STATE_DISCONNECTED)
return;
if (next_state_ == STATE_CONNECT_COMPLETE)
@ -624,19 +599,6 @@ int QuicProxyClientSocket::ProcessResponseHeaders(
return OK;
}
void QuicProxyClientSocket::FailPendingReadOnFastOpenFailure(int error) {
if (read_callback_.is_null()) {
// No pending application read; still cancel the response stream so a
// late body cannot be delivered after the failed CONNECT response.
stream_->Reset(quic::QUIC_STREAM_CANCELLED);
return;
}
read_buf_ = nullptr;
stream_->Reset(quic::QUIC_STREAM_CANCELLED);
// May destroy |this|; run last and use no members afterwards.
std::move(read_callback_).Run(error);
}
void QuicProxyClientSocket::OnBeforeTunnelRequestComplete(
base::expected<HttpRequestHeaders, Error> result) {
if (result.has_value()) {

View File

@ -106,16 +106,6 @@ class NET_EXPORT_PRIVATE QuicProxyClientSocket : public ProxyClientSocket {
void OnReadResponseHeadersComplete(int result);
int ProcessResponseHeaders(const quiche::HttpHeaderBlock& headers);
// Fast Open async failure path: a Fast Open Connect() can complete before
// the app has issued a Read(); when the CONNECT response then fails
// asynchronously, a pending application Read() must observe the failure
// instead of waiting indefinitely for a stream close that may never come.
// Must be called after next_state_ is set to STATE_DISCONNECTED. The read
// callback may destroy |this|, so DoLoop must not touch members afterwards
// (its loop condition short-circuits on the ERR_IO_PENDING the caller sets
// next).
void FailPendingReadOnFastOpenFailure(int error);
// Callback for proxy_delegate_->OnBeforeTunnelRequest().
void OnBeforeTunnelRequestComplete(
base::expected<HttpRequestHeaders, Error> result);
@ -179,7 +169,7 @@ class NET_EXPORT_PRIVATE QuicProxyClientSocket : public ProxyClientSocket {
bool use_fastopen_;
std::optional<size_t> preamble_index_;
bool read_headers_pending_;
std::optional<bool> read_headers_pending_;
const NetLogWithSource net_log_;