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

cmcfarlen pushed a commit to branch 10.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit dc2d858310f490d27fddc5cbb3799f4e1bc4a39f
Author: Masaori Koshiba <[email protected]>
AuthorDate: Tue May 19 09:29:37 2026 +0900

    Fix RangeTransform on stale-revalidate (#13158)
    
    * Fix RangeTransform on stale-revalidate
    
    * Fix comments and add grown response test case
    
    * Add error cases of range-not-satisfiable
    
    * Fix build
    
    (cherry picked from commit c21bb60756d7fb7fb2c0419b4c3d0ffad8ba6077)
---
 src/proxy/Transform.cc                             |   8 +-
 src/proxy/http/HttpSM.cc                           |  36 ++++
 tests/gold_tests/headers/range_transform.test.py   |  18 ++
 .../headers/replays/range_transform.replay.yaml    | 223 +++++++++++++++++++++
 4 files changed, 282 insertions(+), 3 deletions(-)

diff --git a/src/proxy/Transform.cc b/src/proxy/Transform.cc
index 4269595213..619e09e90b 100644
--- a/src/proxy/Transform.cc
+++ b/src/proxy/Transform.cc
@@ -808,7 +808,7 @@ RangeTransform::RangeTransform(ProxyMutex *mut, RangeRecord 
*ranges, int num_fie
   SET_HANDLER(&RangeTransform::handle_event);
 
   m_num_chars_for_cl = num_chars_for_int(m_range_content_length);
-  Dbg(dbg_ctl_http_trans, "RangeTransform creation finishes");
+  Dbg(dbg_ctl_http_trans, "RangeTransform init: %" PRId64 "-%" PRId64 "/%" 
PRId64, ranges->_start, ranges->_end, content_length);
 }
 
 /*-------------------------------------------------------------------------
@@ -925,8 +925,9 @@ RangeTransform::transform_to_range()
 
       if (toskip > 0) {
         reader->consume(toskip);
-        *done_byte += toskip;
-        avail       = reader->read_avail();
+        m_write_vio.ndone += toskip;
+        *done_byte        += toskip;
+        avail              = reader->read_avail();
       }
     }
 
@@ -939,6 +940,7 @@ RangeTransform::transform_to_range()
 
       m_output_buf->write(reader, tosend);
       reader->consume(tosend);
+      m_write_vio.ndone += tosend;
 
       m_done     += tosend;
       *done_byte += tosend;
diff --git a/src/proxy/http/HttpSM.cc b/src/proxy/http/HttpSM.cc
index 7f6e2f798c..71a7b8181c 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -5089,6 +5089,42 @@ HttpSM::do_range_setup_if_necessary()
         if (t_state.cache_info.action == HttpTransact::CacheAction_t::REPLACE) 
{
           if (t_state.hdr_info.server_response.status_get() == HTTPStatus::OK) 
{
             Dbg(dbg_ctl_http_range, "Serving transform after stale cache 
re-serve");
+
+            // Ranges and range_output_cl were computed against the stale 
cached object size. If the fresh origin Content-Length
+            // differs, re-parse the Range against the fresh value so the 
outgoing Content-Length/Content-Range match the body
+            // actually being sent. Without this, Content-Length/Content-Range 
advertise the stale cached size.
+            const int64_t fresh_cl = 
t_state.hdr_info.server_response.get_content_length();
+            if (fresh_cl == 0) {
+              // Re-parse yielded e.g. RANGE_NOT_SATISFIABLE (entire range 
past fresh body); let downstream handling take over
+              // without installing the transform.
+              Dbg(dbg_ctl_http_range, "Not transforming: fresh response body 
is empty");
+              return;
+            }
+            const int64_t cached_cl = t_state.cache_info.object_read ? 
t_state.cache_info.object_read->object_size_get() : -1;
+            if (fresh_cl != cached_cl) {
+              SMDbg(dbg_ctl_http_range, "Re-parsing range against fresh origin 
Content-Length %" PRId64 " (was %" PRId64 ")",
+                    fresh_cl, cached_cl);
+              delete[] t_state.ranges;
+              t_state.ranges           = nullptr;
+              t_state.num_range_fields = 0;
+              t_state.range_setup      = HttpTransact::RangeSetup_t::NONE;
+              t_state.range_output_cl  = 0;
+              parse_range_done         = false;
+
+              std::string_view content_type =
+                
t_state.hdr_info.server_response.value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE));
+              parse_range_and_compare(field, fresh_cl);
+              calculate_output_cl(content_type.length(), 
num_chars_for_int(fresh_cl));
+
+              if (t_state.range_setup != 
HttpTransact::RangeSetup_t::REQUESTED) {
+                // Re-parse yielded e.g. RANGE_NOT_SATISFIABLE (entire range 
past fresh body); let downstream handling take over
+                // without installing the transform.
+                Dbg(dbg_ctl_http_range, "Not transforming: 
parse_range_and_compare set t_state.range_setup=%d",
+                    static_cast<int>(HttpTransact::RangeSetup_t::REQUESTED));
+                return;
+              }
+            }
+
             do_transform = true;
           } else {
             Dbg(dbg_ctl_http_range, "Not transforming after revalidate");
diff --git a/tests/gold_tests/headers/range_transform.test.py 
b/tests/gold_tests/headers/range_transform.test.py
new file mode 100644
index 0000000000..63ef0ea803
--- /dev/null
+++ b/tests/gold_tests/headers/range_transform.test.py
@@ -0,0 +1,18 @@
+#  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.
+
+tr = Test.ATSReplayTest(replay_file="replays/range_transform.replay.yaml")
+tr.Processes.Default.TimeOut = 10
diff --git a/tests/gold_tests/headers/replays/range_transform.replay.yaml 
b/tests/gold_tests/headers/replays/range_transform.replay.yaml
new file mode 100644
index 0000000000..d4376dfabc
--- /dev/null
+++ b/tests/gold_tests/headers/replays/range_transform.replay.yaml
@@ -0,0 +1,223 @@
+#  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.
+
+# Verify RangeTransform on stale-revalidate
+#
+# Preconditions for the bug:
+#  1. Client request has a `Range` header.
+#  2. Cached object is stale, so ATS revalidates.
+#  3. Origin returns `200 OK` with the full body (not 304, not 206).
+#  4. The fresh `Content-Length` differs from the cached object size.
+
+meta:
+  version: '1.0'
+
+autest:
+  description: 'Verify RangeTransform'
+
+  dns:
+    name: "dns-range-transform"
+
+  server:
+    name: "server-range-transform"
+
+  client:
+    name: "client-range-transform"
+
+  ats:
+    name: "ts-range-transform"
+
+    process_config:
+      enable_cache: true
+
+    records_config:
+      proxy.config.http.wait_for_cache: 1
+      proxy.config.http.cache.required_headers: 0
+      proxy.config.diags.debug.enabled: 1
+      proxy.config.diags.debug.tags: 'http'
+
+    remap_config:
+      - from: "http://example.com/";
+        to: "http://backend.example.com:{SERVER_HTTP_PORT}/";
+
+    log_validation:
+      traffic_out:
+        contains:
+          - expression: 'perform_transform_cache_write_action 
CacheAction_t::REPLACE'
+            description: 'Stale cache is replaced via RangeTransform'
+
+sessions:
+  # Prime cache with BIG body.
+  - transactions:
+      - client-request:
+          method: "GET"
+          version: "1.1"
+          url: /obj
+          headers:
+            fields:
+              - [Host, example.com]
+              - [uuid, prime]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Date, "Mon, 01 Jan 2026 00:00:00 GMT"]
+              - [Cache-Control, "max-age=1, public"]
+              - [Content-Type, application/octet-stream]
+              - [Content-Length, 64097]
+          content:
+            size: 64097
+
+        proxy-response:
+          status: 200
+
+  # Stale revalidate: origin body shrunk to 40000, Range end (64096) 
unreachable.
+  - transactions:
+      - client-request:
+          delay: 1500ms
+          method: "GET"
+          version: "1.1"
+          url: /obj
+          headers:
+            fields:
+              - [Host, example.com]
+              - [uuid, range-revalidate]
+              - [Range, "bytes=0-64096"]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Date, "Mon, 01 Jan 2026 00:00:05 GMT"]
+              - [Cache-Control, "max-age=1, public"]
+              - [Content-Type, application/octet-stream]
+              - [Content-Length, 40000]
+          content:
+            size: 40000
+
+        proxy-response:
+          status: 206
+          reason: Partial Content
+          headers:
+            fields:
+              - [Content-Range, {value: "bytes 0-39999/40000", as: equal}]
+              - [Content-Length, {value: 40000, as: equal}]
+
+
+  # Stale revalidate: origin body grown to 80000 (larger than cached). Range 
stays
+  # satisfiable, but Content-Range total must reflect fresh 80000, not stale 
size.
+  - transactions:
+      - client-request:
+          delay: 1500ms
+          method: "GET"
+          version: "1.1"
+          url: /obj
+          headers:
+            fields:
+              - [Host, example.com]
+              - [uuid, range-revalidate-grown]
+              - [Range, "bytes=0-64096"]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Date, "Mon, 01 Jan 2026 00:00:10 GMT"]
+              - [Cache-Control, "max-age=1, public"]
+              - [Content-Type, application/octet-stream]
+              - [Content-Length, 80000]
+          content:
+            size: 80000
+
+        proxy-response:
+          status: 206
+          reason: Partial Content
+          headers:
+            fields:
+              - [Content-Range, {value: "bytes 0-64096/80000", as: equal}]
+              - [Content-Length, {value: 64097, as: equal}]
+
+  # Error Cases: when requested range is unsatisfiable, choices are below from 
RFC 9110. Our choice is B.
+  #   A). Return 416 Range Not Satisfiable
+  #   B). Ignore Range header, return 200 OK
+
+  # Stale revalidate: new origin body is empty
+  - transactions:
+      - client-request:
+          delay: 1500ms
+          method: "GET"
+          version: "1.1"
+          url: /obj
+          headers:
+            fields:
+              - [Host, example.com]
+              - [uuid, range-revalidate-empty]
+              - [Range, "bytes=0-64096"]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Date, "Mon, 01 Jan 2026 00:00:20 GMT"]
+              - [Cache-Control, "max-age=1, public"]
+              - [Content-Type, application/octet-stream]
+              - [Content-Length, 0]
+          content:
+            size: 0
+
+        proxy-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 40000]
+
+  # Stale revalidate: new origin body is smaller than requested range
+  - transactions:
+      - client-request:
+          delay: 1500ms
+          method: "GET"
+          version: "1.1"
+          url: /obj
+          headers:
+            fields:
+              - [Host, example.com]
+              - [uuid, range-revalidate-out-of-range]
+              - [Range, "bytes=60000-64096"]
+
+        server-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Date, "Mon, 01 Jan 2026 00:00:30 GMT"]
+              - [Cache-Control, "max-age=1, public"]
+              - [Content-Type, application/octet-stream]
+              - [Content-Length, 40000]
+          content:
+            size: 40000
+
+        proxy-response:
+          status: 200
+          reason: OK
+          headers:
+            fields:
+              - [Content-Length, 40000]

Reply via email to