Skip to content

Commit c21bb60

Browse files
authored
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
1 parent 0dc18dd commit c21bb60

4 files changed

Lines changed: 282 additions & 3 deletions

File tree

src/proxy/Transform.cc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ RangeTransform::RangeTransform(ProxyMutex *mut, RangeRecord *ranges, int num_fie
808808
SET_HANDLER(&RangeTransform::handle_event);
809809

810810
m_num_chars_for_cl = num_chars_for_int(m_range_content_length);
811-
Dbg(dbg_ctl_http_trans, "RangeTransform creation finishes");
811+
Dbg(dbg_ctl_http_trans, "RangeTransform init: %" PRId64 "-%" PRId64 "/%" PRId64, ranges->_start, ranges->_end, content_length);
812812
}
813813

814814
/*-------------------------------------------------------------------------
@@ -925,8 +925,9 @@ RangeTransform::transform_to_range()
925925

926926
if (toskip > 0) {
927927
reader->consume(toskip);
928-
*done_byte += toskip;
929-
avail = reader->read_avail();
928+
m_write_vio.ndone += toskip;
929+
*done_byte += toskip;
930+
avail = reader->read_avail();
930931
}
931932
}
932933

@@ -939,6 +940,7 @@ RangeTransform::transform_to_range()
939940

940941
m_output_buf->write(reader, tosend);
941942
reader->consume(tosend);
943+
m_write_vio.ndone += tosend;
942944

943945
m_done += tosend;
944946
*done_byte += tosend;

src/proxy/http/HttpSM.cc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5092,6 +5092,42 @@ HttpSM::do_range_setup_if_necessary()
50925092
if (t_state.cache_info.action == HttpTransact::CacheAction_t::REPLACE) {
50935093
if (t_state.hdr_info.server_response.status_get() == HTTPStatus::OK) {
50945094
Dbg(dbg_ctl_http_range, "Serving transform after stale cache re-serve");
5095+
5096+
// Ranges and range_output_cl were computed against the stale cached object size. If the fresh origin Content-Length
5097+
// differs, re-parse the Range against the fresh value so the outgoing Content-Length/Content-Range match the body
5098+
// actually being sent. Without this, Content-Length/Content-Range advertise the stale cached size.
5099+
const int64_t fresh_cl = t_state.hdr_info.server_response.get_content_length();
5100+
if (fresh_cl == 0) {
5101+
// Re-parse yielded e.g. RANGE_NOT_SATISFIABLE (entire range past fresh body); let downstream handling take over
5102+
// without installing the transform.
5103+
Dbg(dbg_ctl_http_range, "Not transforming: fresh response body is empty");
5104+
return;
5105+
}
5106+
const int64_t cached_cl = t_state.cache_info.object_read ? t_state.cache_info.object_read->object_size_get() : -1;
5107+
if (fresh_cl != cached_cl) {
5108+
SMDbg(dbg_ctl_http_range, "Re-parsing range against fresh origin Content-Length %" PRId64 " (was %" PRId64 ")",
5109+
fresh_cl, cached_cl);
5110+
delete[] t_state.ranges;
5111+
t_state.ranges = nullptr;
5112+
t_state.num_range_fields = 0;
5113+
t_state.range_setup = HttpTransact::RangeSetup_t::NONE;
5114+
t_state.range_output_cl = 0;
5115+
parse_range_done = false;
5116+
5117+
std::string_view content_type =
5118+
t_state.hdr_info.server_response.value_get(static_cast<std::string_view>(MIME_FIELD_CONTENT_TYPE));
5119+
parse_range_and_compare(field, fresh_cl);
5120+
calculate_output_cl(content_type.length(), num_chars_for_int(fresh_cl));
5121+
5122+
if (t_state.range_setup != HttpTransact::RangeSetup_t::REQUESTED) {
5123+
// Re-parse yielded e.g. RANGE_NOT_SATISFIABLE (entire range past fresh body); let downstream handling take over
5124+
// without installing the transform.
5125+
Dbg(dbg_ctl_http_range, "Not transforming: parse_range_and_compare set t_state.range_setup=%d",
5126+
static_cast<int>(HttpTransact::RangeSetup_t::REQUESTED));
5127+
return;
5128+
}
5129+
}
5130+
50955131
do_transform = true;
50965132
} else {
50975133
Dbg(dbg_ctl_http_range, "Not transforming after revalidate");
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
tr = Test.ATSReplayTest(replay_file="replays/range_transform.replay.yaml")
18+
tr.Processes.Default.TimeOut = 10
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
# Verify RangeTransform on stale-revalidate
18+
#
19+
# Preconditions for the bug:
20+
# 1. Client request has a `Range` header.
21+
# 2. Cached object is stale, so ATS revalidates.
22+
# 3. Origin returns `200 OK` with the full body (not 304, not 206).
23+
# 4. The fresh `Content-Length` differs from the cached object size.
24+
25+
meta:
26+
version: '1.0'
27+
28+
autest:
29+
description: 'Verify RangeTransform'
30+
31+
dns:
32+
name: "dns-range-transform"
33+
34+
server:
35+
name: "server-range-transform"
36+
37+
client:
38+
name: "client-range-transform"
39+
40+
ats:
41+
name: "ts-range-transform"
42+
43+
process_config:
44+
enable_cache: true
45+
46+
records_config:
47+
proxy.config.http.wait_for_cache: 1
48+
proxy.config.http.cache.required_headers: 0
49+
proxy.config.diags.debug.enabled: 1
50+
proxy.config.diags.debug.tags: 'http'
51+
52+
remap_config:
53+
- from: "http://example.com/"
54+
to: "http://backend.example.com:{SERVER_HTTP_PORT}/"
55+
56+
log_validation:
57+
traffic_out:
58+
contains:
59+
- expression: 'perform_transform_cache_write_action CacheAction_t::REPLACE'
60+
description: 'Stale cache is replaced via RangeTransform'
61+
62+
sessions:
63+
# Prime cache with BIG body.
64+
- transactions:
65+
- client-request:
66+
method: "GET"
67+
version: "1.1"
68+
url: /obj
69+
headers:
70+
fields:
71+
- [Host, example.com]
72+
- [uuid, prime]
73+
74+
server-response:
75+
status: 200
76+
reason: OK
77+
headers:
78+
fields:
79+
- [Date, "Mon, 01 Jan 2026 00:00:00 GMT"]
80+
- [Cache-Control, "max-age=1, public"]
81+
- [Content-Type, application/octet-stream]
82+
- [Content-Length, 64097]
83+
content:
84+
size: 64097
85+
86+
proxy-response:
87+
status: 200
88+
89+
# Stale revalidate: origin body shrunk to 40000, Range end (64096) unreachable.
90+
- transactions:
91+
- client-request:
92+
delay: 1500ms
93+
method: "GET"
94+
version: "1.1"
95+
url: /obj
96+
headers:
97+
fields:
98+
- [Host, example.com]
99+
- [uuid, range-revalidate]
100+
- [Range, "bytes=0-64096"]
101+
102+
server-response:
103+
status: 200
104+
reason: OK
105+
headers:
106+
fields:
107+
- [Date, "Mon, 01 Jan 2026 00:00:05 GMT"]
108+
- [Cache-Control, "max-age=1, public"]
109+
- [Content-Type, application/octet-stream]
110+
- [Content-Length, 40000]
111+
content:
112+
size: 40000
113+
114+
proxy-response:
115+
status: 206
116+
reason: Partial Content
117+
headers:
118+
fields:
119+
- [Content-Range, {value: "bytes 0-39999/40000", as: equal}]
120+
- [Content-Length, {value: 40000, as: equal}]
121+
122+
123+
# Stale revalidate: origin body grown to 80000 (larger than cached). Range stays
124+
# satisfiable, but Content-Range total must reflect fresh 80000, not stale size.
125+
- transactions:
126+
- client-request:
127+
delay: 1500ms
128+
method: "GET"
129+
version: "1.1"
130+
url: /obj
131+
headers:
132+
fields:
133+
- [Host, example.com]
134+
- [uuid, range-revalidate-grown]
135+
- [Range, "bytes=0-64096"]
136+
137+
server-response:
138+
status: 200
139+
reason: OK
140+
headers:
141+
fields:
142+
- [Date, "Mon, 01 Jan 2026 00:00:10 GMT"]
143+
- [Cache-Control, "max-age=1, public"]
144+
- [Content-Type, application/octet-stream]
145+
- [Content-Length, 80000]
146+
content:
147+
size: 80000
148+
149+
proxy-response:
150+
status: 206
151+
reason: Partial Content
152+
headers:
153+
fields:
154+
- [Content-Range, {value: "bytes 0-64096/80000", as: equal}]
155+
- [Content-Length, {value: 64097, as: equal}]
156+
157+
# Error Cases: when requested range is unsatisfiable, choices are below from RFC 9110. Our choice is B.
158+
# A). Return 416 Range Not Satisfiable
159+
# B). Ignore Range header, return 200 OK
160+
161+
# Stale revalidate: new origin body is empty
162+
- transactions:
163+
- client-request:
164+
delay: 1500ms
165+
method: "GET"
166+
version: "1.1"
167+
url: /obj
168+
headers:
169+
fields:
170+
- [Host, example.com]
171+
- [uuid, range-revalidate-empty]
172+
- [Range, "bytes=0-64096"]
173+
174+
server-response:
175+
status: 200
176+
reason: OK
177+
headers:
178+
fields:
179+
- [Date, "Mon, 01 Jan 2026 00:00:20 GMT"]
180+
- [Cache-Control, "max-age=1, public"]
181+
- [Content-Type, application/octet-stream]
182+
- [Content-Length, 0]
183+
content:
184+
size: 0
185+
186+
proxy-response:
187+
status: 200
188+
reason: OK
189+
headers:
190+
fields:
191+
- [Content-Length, 40000]
192+
193+
# Stale revalidate: new origin body is smaller than requested range
194+
- transactions:
195+
- client-request:
196+
delay: 1500ms
197+
method: "GET"
198+
version: "1.1"
199+
url: /obj
200+
headers:
201+
fields:
202+
- [Host, example.com]
203+
- [uuid, range-revalidate-out-of-range]
204+
- [Range, "bytes=60000-64096"]
205+
206+
server-response:
207+
status: 200
208+
reason: OK
209+
headers:
210+
fields:
211+
- [Date, "Mon, 01 Jan 2026 00:00:30 GMT"]
212+
- [Cache-Control, "max-age=1, public"]
213+
- [Content-Type, application/octet-stream]
214+
- [Content-Length, 40000]
215+
content:
216+
size: 40000
217+
218+
proxy-response:
219+
status: 200
220+
reason: OK
221+
headers:
222+
fields:
223+
- [Content-Length, 40000]

0 commit comments

Comments
 (0)