This is an automated email from the ASF dual-hosted git repository.
masaori335 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 c21bb60756 Fix RangeTransform on stale-revalidate (#13158)
c21bb60756 is described below
commit c21bb60756d7fb7fb2c0419b4c3d0ffad8ba6077
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
---
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 b2b9abbb0e..109593b70e 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 f1a113047a..7b69aaed8a 100644
--- a/src/proxy/http/HttpSM.cc
+++ b/src/proxy/http/HttpSM.cc
@@ -5092,6 +5092,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]