Skip to content

Commit e88bb08

Browse files
Match standard HTTP semantics of inclusive end value in Range and Content-Range headers (#7643)
Co-authored-by: Amaury Chamayou <amchamay@microsoft.com>
1 parent 6a00156 commit e88bb08

4 files changed

Lines changed: 106 additions & 16 deletions

File tree

include/ccf/http_consts.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace ccf
2020
static constexpr auto DIGEST = "digest";
2121
static constexpr auto HOST = "host";
2222
static constexpr auto LOCATION = "location";
23+
static constexpr auto RANGE = "range";
2324
static constexpr auto RETRY_AFTER = "retry-after";
2425
static constexpr auto TRAILER = "trailer";
2526
static constexpr auto WWW_AUTHENTICATE = "www-authenticate";

src/node/rpc/file_serving_handlers.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,8 @@ namespace ccf::node
160160
size_t range_start = 0;
161161
size_t range_end = total_size;
162162
{
163-
const auto range_header = ctx.rpc_ctx->get_request_header("range");
163+
const auto range_header =
164+
ctx.rpc_ctx->get_request_header(ccf::http::headers::RANGE);
164165
if (range_header.has_value())
165166
{
166167
LOG_TRACE_FMT("Parsing range header {}", range_header.value());
@@ -231,10 +232,14 @@ namespace ccf::node
231232

232233
if (!s_range_end.empty())
233234
{
235+
// Range end in header is inclusive, but we prefer to reason about
236+
// exclusive range end (ie - one past the end)
237+
size_t inclusive_range_end = 0;
238+
234239
// Fully-specified range, like "X-Y"
235240
{
236241
const auto [p, ec] = std::from_chars(
237-
s_range_end.begin(), s_range_end.end(), range_end);
242+
s_range_end.begin(), s_range_end.end(), inclusive_range_end);
238243
if (ec != std::errc())
239244
{
240245
ctx.rpc_ctx->set_error(
@@ -248,6 +253,8 @@ namespace ccf::node
248253
}
249254
}
250255

256+
range_end = inclusive_range_end + 1;
257+
251258
if (range_end > total_size)
252259
{
253260
LOG_DEBUG_FMT(
@@ -333,11 +340,15 @@ namespace ccf::node
333340
ccf::http::headervalues::contenttype::OCTET_STREAM);
334341
ctx.rpc_ctx->set_response_body(std::move(contents));
335342

343+
// Convert back to HTTP-style inclusive range end
344+
const auto inclusive_range_end = range_end - 1;
345+
336346
// Partial Content responses describe the current response in
337347
// Content-Range
338348
ctx.rpc_ctx->set_response_header(
339349
ccf::http::headers::CONTENT_RANGE,
340-
fmt::format("bytes {}-{}/{}", range_start, range_end, total_size));
350+
fmt::format(
351+
"bytes {}-{}/{}", range_start, inclusive_range_end, total_size));
341352
}
342353

343354
static void init_file_serving_handlers(

src/snapshots/fetch.h

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ namespace snapshots
4343
struct ContentRangeHeader
4444
{
4545
size_t range_start;
46-
size_t range_end;
46+
size_t inclusive_range_end;
4747
size_t total_size;
4848
};
4949

@@ -93,7 +93,7 @@ namespace snapshots
9393

9494
{
9595
const auto [p, ec] = std::from_chars(
96-
range_end.begin(), range_end.end(), parsed_values.range_end);
96+
range_end.begin(), range_end.end(), parsed_values.inclusive_range_end);
9797
if (ec != std::errc())
9898
{
9999
throw std::runtime_error(fmt::format(
@@ -115,6 +115,65 @@ namespace snapshots
115115
}
116116
}
117117

118+
{
119+
// Use content-length to determine whether the sender used an exclusive or
120+
// inclusive range end
121+
auto length_it = headers.find(ccf::http::headers::CONTENT_LENGTH);
122+
if (length_it == headers.end())
123+
{
124+
throw std::runtime_error(
125+
"Response is missing expected content-length header");
126+
}
127+
128+
size_t content_length = 0;
129+
130+
{
131+
const auto& length_s = length_it->second;
132+
133+
const auto [p, ec] = std::from_chars(
134+
length_s.data(), length_s.data() + length_s.size(), content_length);
135+
136+
if (ec != std::errc())
137+
{
138+
throw std::runtime_error(fmt::format(
139+
"Could not parse length from content-length header: {}",
140+
length_it->second));
141+
}
142+
}
143+
144+
const auto range_length =
145+
parsed_values.inclusive_range_end - parsed_values.range_start;
146+
if (range_length == content_length)
147+
{
148+
LOG_INFO_FMT(
149+
"Server sent an exclusive-end content-range header. "
150+
"content-length={}, content-range={}. Adjusting this to local "
151+
"inclusive-end representation. This should be a temporary "
152+
"mismatch, between 6.x and 7.x nodes in a mixed network",
153+
length_it->second,
154+
it->second);
155+
parsed_values.inclusive_range_end -= 1;
156+
}
157+
else if (range_length + 1 == content_length)
158+
{
159+
LOG_DEBUG_FMT(
160+
"Server sent an inclusive-end content-range header. "
161+
"content-length={}, content-range={}. This is expected for 7.x to "
162+
"7.x nodes",
163+
length_it->second,
164+
it->second);
165+
}
166+
else
167+
{
168+
throw std::runtime_error(fmt::format(
169+
"content-range ({}, {} bytes) and content-length ({}) headers do not "
170+
"agree",
171+
it->second,
172+
range_length + 1,
173+
length_it->second));
174+
}
175+
}
176+
118177
return parsed_values;
119178
}
120179

@@ -142,6 +201,7 @@ namespace snapshots
142201
constexpr size_t range_size = 4L * 1024 * 1024;
143202
size_t range_start = 0;
144203
size_t range_end = range_size;
204+
size_t inclusive_range_end = range_end - 1;
145205
bool fetched_all = false;
146206

147207
auto process_partial_response =
@@ -160,26 +220,29 @@ namespace snapshots
160220

161221
// The server may give us _less_ than we requested (since they know
162222
// where the file ends), but should never give us more
163-
if (content_range.range_end > range_end)
223+
if (content_range.inclusive_range_end > inclusive_range_end)
164224
{
165225
throw std::runtime_error(fmt::format(
166226
"Unexpected range response. Requested bytes {}-{}, received "
167227
"range ending at {}",
168228
range_start,
169-
range_end,
170-
content_range.range_end));
229+
inclusive_range_end,
230+
content_range.inclusive_range_end));
171231
}
172232

233+
const auto content_range_exclusive_range_end =
234+
content_range.inclusive_range_end + 1;
235+
173236
const auto range_size =
174-
content_range.range_end - content_range.range_start;
237+
content_range_exclusive_range_end - content_range.range_start;
175238
LOG_TRACE_FMT(
176239
"Received {}-byte chunk from {}. Now have {}/{}",
177240
range_size,
178241
request.get_url(),
179-
content_range.range_end,
242+
content_range_exclusive_range_end,
180243
content_range.total_size);
181244

182-
if (content_range.range_end == content_range.total_size)
245+
if (content_range_exclusive_range_end == content_range.total_size)
183246
{
184247
fetched_all = true;
185248
}
@@ -188,6 +251,7 @@ namespace snapshots
188251
// Advance range for next request
189252
range_start = range_end;
190253
range_end = range_start + range_size;
254+
inclusive_range_end = range_end - 1;
191255
}
192256
};
193257

@@ -203,7 +267,8 @@ namespace snapshots
203267

204268
ccf::curl::UniqueSlist headers;
205269
headers.append(
206-
"Range", fmt::format("bytes={}-{}", range_start, range_end));
270+
ccf::http::headers::RANGE,
271+
fmt::format("bytes={}-{}", range_start, inclusive_range_end));
207272

208273
CURLcode curl_response = CURLE_FAILED_INIT;
209274
long status_code = 0;
@@ -281,7 +346,8 @@ namespace snapshots
281346
{
282347
ccf::curl::UniqueSlist headers;
283348
headers.append(
284-
"Range", fmt::format("bytes={}-{}", range_start, range_end));
349+
ccf::http::headers::RANGE,
350+
fmt::format("bytes={}-{}", range_start, inclusive_range_end));
285351

286352
std::unique_ptr<ccf::curl::CurlRequest> snapshot_range_request;
287353
CURLcode curl_response = CURLE_OK;

tests/e2e_operations.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,25 +357,37 @@ def do_request(http_verb, *args, **kwargs):
357357
assert r.headers["accept-ranges"] == "bytes", r.headers
358358
total_size = int(r.headers["content-length"])
359359

360+
# Use HTTP-style inclusive range end value
361+
range_max = total_size - 1
362+
360363
a = total_size // 3
361364
b = a * 2
362365
for start, end in [
363366
(0, None),
364-
(0, total_size),
367+
(0, 0),
368+
(0, range_max),
365369
(0, a),
366370
(a, a),
367371
(a, b),
368372
(b, b),
369-
(b, total_size),
373+
(b, range_max),
370374
(b, None),
375+
(range_max, range_max),
371376
]:
372377
range_header_value = f"{start}-{'' if end is None else end}"
373378
r = do_request(
374379
"GET", path, headers={"range": f"bytes={range_header_value}"}
375380
)
376381
assert r.status_code == http.HTTPStatus.PARTIAL_CONTENT.value, r
382+
headers = r.headers
383+
implied_end = range_max if end is None else end
384+
assert int(headers["content-length"]) == implied_end - start + 1
385+
assert (
386+
headers["content-range"]
387+
== f"bytes {start}-{implied_end}/{total_size}"
388+
)
377389

378-
expected = snapshot_data[start:end]
390+
expected = snapshot_data[start : (None if end is None else end + 1)]
379391
actual = r.body.data()
380392
assert (
381393
expected == actual

0 commit comments

Comments
 (0)