Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/proxy/Transform.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/*-------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions src/proxy/http/HttpSM.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5091,6 +5091,35 @@ 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();
const int64_t cached_cl = t_state.cache_info.object_read ? t_state.cache_info.object_read->object_size_get() : -1;
if (fresh_cl > 0 && fresh_cl != cached_cl) {
Comment on lines +5096 to +5100
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.
Comment on lines +5116 to +5118
return;
}
}

do_transform = true;
} else {
Dbg(dbg_ctl_http_range, "Not transforming after revalidate");
Expand Down
18 changes: 18 additions & 0 deletions tests/gold_tests/headers/range_transform.test.py
Original file line number Diff line number Diff line change
@@ -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
155 changes: 155 additions & 0 deletions tests/gold_tests/headers/replays/range_transform.replay.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 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 2024 00:00:05 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}]