This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 31eb68f6fb Shut the client read side down per transaction, not per 
connection (#13523)
31eb68f6fb is described below

commit 31eb68f6fb07f105ee90ae062524e8b8f364b91d
Author: Brian Neradt <[email protected]>
AuthorDate: Mon Aug 10 11:35:32 2026 -0500

    Shut the client read side down per transaction, not per connection (#13523)
    
    HttpSM::state_watch_for_client_abort reached past the transaction to
    _ua.get_txn()->get_netvc() to half close the client read side on an early
    EOS. For HTTP/2 and HTTP/3 that NetVConnection is shared by every stream
    on the connection, so a single aborted stream stopped the session from
    reading frames for all of the others. Route the shutdown through the
    transaction instead: Http2Stream and HQTransaction already implement
    do_io_shutdown() as a deliberate no-op for exactly this reason, and
    HTTP/1.x is unaffected because ProxyTransaction forwards to the session's
    NetVConnection.
    
    This is the remaining half of #12529. That change was written to address
    two regressions from #12502, the second being "HTTP/2 connection is
    closed if a stream is reset even if other streams are alive", with the
    stated approach of calling ProxyTransaction::do_io_shutdown() instead of
    NetVConnection::do_io_shutdown(). It converted the two branches it added
    but left the pre-existing IO_SHUTDOWN_READ branch calling the
    NetVConnection directly, so the connection-wide shutdown survived for the
    case where the tunnel still has a consumer besides the client. A response
    transform reaches that case readily: the transform stage runs the whole
    body before anything is written back, so the stream has no write of its
    own and a client reset arrives as an EOS on the stream's read VIO.
    
    Clearing the session's read VIO buffer is not inert, because
    Http2CommonSession still holds that VIO and re-enables it every 128
    frames through HTTP2_SESSION_EVENT_REENABLE. A release build then takes
    the ntodo() <= 0 path in net_read_io and the connection stalls silently,
    failing every in-flight stream; a debug build aborts on
    SSLNetVConnection's `ink_assert(buf.writer())`, the assertion reported
    in #9448.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 src/proxy/http/HttpSM.cc                           |  11 +-
 ...ttp2_client_reset_keeps_session_reading.test.py |  23 ++++
 ..._client_reset_keeps_session_reading.replay.yaml | 149 +++++++++++++++++++++
 3 files changed, 182 insertions(+), 1 deletion(-)

diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 1ffa2d8b5b..7a8f59086a 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -905,10 +905,19 @@ HttpSM::state_watch_for_client_abort(int event, void 
*data)
   case VC_EVENT_EOS: {
     // We got an early EOS.
     if (!terminate_sm) { // Not done already
+      // ProxySession::do_io_shutdown dereferences its NetVConnection
+      // unconditionally, so only shut down while the peer is still attached.
       NetVConnection *netvc = _ua.get_txn()->get_netvc();
+
       if (_ua.get_txn()->allow_half_open() || 
tunnel.has_consumer_besides_client()) {
         if (netvc) {
-          netvc->do_io_shutdown(IO_SHUTDOWN_READ);
+          // Shut the read side down through the transaction rather than 
through
+          // the NetVConnection. For multiplexed protocols the NetVConnection 
is
+          // shared by every stream on the connection, so shutting its read 
side
+          // down here would stop the session from reading frames for all of 
the
+          // other streams. HTTP/2 and HTTP/3 therefore implement
+          // do_io_shutdown() as a no-op.
+          _ua.get_txn()->do_io_shutdown(IO_SHUTDOWN_READ);
         }
       } else if (t_state.txn_conf->cache_http &&
                  (server_entry != nullptr && server_entry->vc_read_handler == 
&HttpSM::state_read_server_response_header)) {
diff --git 
a/tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py 
b/tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py
new file mode 100644
index 0000000000..1e20b5aae6
--- /dev/null
+++ b/tests/gold_tests/h2/http2_client_reset_keeps_session_reading.test.py
@@ -0,0 +1,23 @@
+'''Verify an HTTP/2 stream reset does not stop the session from reading.'''
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+Test.Summary = __doc__
+
+Test.SkipUnless(
+    Condition.HasOpenSSLVersion('1.1.1'), 
Condition.HasProxyVerifierVersion('2.8.0'), 
Condition.PluginExists('null_transform.so'))
+
+Test.ATSReplayTest(replay_file="replay/http2_client_reset_keeps_session_reading.replay.yaml")
diff --git 
a/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.replay.yaml
 
b/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.replay.yaml
new file mode 100644
index 0000000000..52974a8c0b
--- /dev/null
+++ 
b/tests/gold_tests/h2/replay/http2_client_reset_keeps_session_reading.replay.yaml
@@ -0,0 +1,149 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+#
+# A response transform makes the tunnel run the whole body through the 
transform
+# before anything is written back to the client. While that is happening the
+# client stream has no write of its own, so a client stream reset is delivered 
as
+# a VC_EVENT_EOS on the stream's read VIO and lands in
+# HttpSM::state_watch_for_client_abort. The transform write consumer is still
+# alive there, so the SM half closes the client read side to let the rest of 
the
+# transaction finish.
+#
+# That shutdown has to apply to the stream, not to the underlying connection: 
for
+# HTTP/2 the NetVConnection is shared by every stream, so shutting its read 
side
+# down stops the session from reading frames for all of the other streams. The
+# second request below is sent after the reset and only gets a response if ATS 
is
+# still reading the connection.
+#
+
+meta:
+  version: '1.0'
+
+autest:
+  description: 'Verify an HTTP/2 stream reset does not stop the session from 
reading'
+
+  server:
+    name: 'server'
+
+  client:
+    name: 'client'
+
+  ats:
+    name: 'ts'
+
+    process_config:
+      enable_tls: true
+      enable_cache: true
+
+    plugin_config:
+      # A response transform so the tunnel has a consumer besides the client
+      # while the body is being read from the origin.
+      - 'null_transform.so'
+
+    records_config:
+      proxy.config.diags.debug.enabled: 1
+      proxy.config.diags.debug.tags: 'http|http_tunnel'
+      # Let the rest of the transaction run for as long as the origin takes.
+      proxy.config.http.background_fill_active_timeout: 0
+      proxy.config.http.background_fill_completed_threshold: 0.0
+      # The origin speaks HTTP/2 so that it can delay the response body without
+      # delaying the response header.
+      proxy.config.ssl.client.alpn_protocols: 'h2,http/1.1'
+      proxy.config.ssl.client.verify.server.policy: 'PERMISSIVE'
+
+    remap_config:
+      - 'map / https://127.0.0.1:{SERVER_HTTPS_PORT}'
+
+    log_validation:
+      traffic_out:
+        contains:
+          - expression: 'adding consumer .transform write.'
+            description: 'Verify the tunnel had a consumer besides the client'
+          - expression: 'state_watch_for_client_abort, VC_EVENT_EOS'
+            description: 'Verify the stream reset was handled as a client 
abort'
+
+sessions:
+- protocol:
+    stack: http2
+    tls:
+      sni: test_sni
+
+  transactions:
+
+  # Stream 1: the origin sends the response header right away and then stalls
+  # before the body, so the transform stage of the tunnel is still running when
+  # the client resets the stream.
+  - client-request:
+      frames:
+      - HEADERS:
+          headers:
+            fields:
+            - [":method", GET]
+            - [":scheme", https]
+            - [":authority", example.data.com]
+            - [":path", /reset-mid-transform]
+            - [uuid, reset-mid-transform]
+      - RST_STREAM:
+          delay: 1s
+          error-code: CANCEL
+
+    server-response:
+      frames:
+      - HEADERS:
+          headers:
+            fields:
+            - [":status", 200]
+            - [Content-Type, text/html]
+            - [Content-Length, '11']
+            - [Cache-Control, 'max-age=300']
+      - DATA:
+          delay: 3s
+          content:
+            encoding: plain
+            data: server_test
+            size: 11
+
+  # Stream 3: sent after the reset above. ATS only sees this request if it is
+  # still reading the connection, so a missing response here means one stream's
+  # abort took the whole connection down with it.
+  - client-request:
+      delay: 2s
+      headers:
+        fields:
+        - [":method", GET]
+        - [":scheme", https]
+        - [":authority", example.data.com]
+        - [":path", /after-the-reset]
+        - [uuid, after-the-reset]
+
+    server-response:
+      headers:
+        fields:
+        - [":status", 200]
+        - [Content-Type, text/html]
+        - [Content-Length, '16']
+        - [X-Response, after-the-reset]
+      content:
+        encoding: plain
+        data: after_the_reset
+        size: 16
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [X-Response, {value: 'after-the-reset', as: equal}]

Reply via email to