Skip to content

Commit b4be36f

Browse files
authored
Match standard HTTP semantics of inclusive end value in Range and Content-Range headers (#7632)
1 parent 0e2a429 commit b4be36f

4 files changed

Lines changed: 47 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: 17 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(
@@ -142,6 +142,7 @@ namespace snapshots
142142
constexpr size_t range_size = 4L * 1024 * 1024;
143143
size_t range_start = 0;
144144
size_t range_end = range_size;
145+
size_t inclusive_range_end = range_end - 1;
145146
bool fetched_all = false;
146147

147148
auto process_partial_response =
@@ -160,26 +161,29 @@ namespace snapshots
160161

161162
// The server may give us _less_ than we requested (since they know
162163
// where the file ends), but should never give us more
163-
if (content_range.range_end > range_end)
164+
if (content_range.inclusive_range_end > inclusive_range_end)
164165
{
165166
throw std::runtime_error(fmt::format(
166167
"Unexpected range response. Requested bytes {}-{}, received "
167168
"range ending at {}",
168169
range_start,
169-
range_end,
170-
content_range.range_end));
170+
inclusive_range_end,
171+
content_range.inclusive_range_end));
171172
}
172173

174+
const auto content_range_exclusive_range_end =
175+
content_range.inclusive_range_end + 1;
176+
173177
const auto range_size =
174-
content_range.range_end - content_range.range_start;
178+
content_range_exclusive_range_end - content_range.range_start;
175179
LOG_TRACE_FMT(
176180
"Received {}-byte chunk from {}. Now have {}/{}",
177181
range_size,
178182
request.get_url(),
179-
content_range.range_end,
183+
content_range_exclusive_range_end,
180184
content_range.total_size);
181185

182-
if (content_range.range_end == content_range.total_size)
186+
if (content_range_exclusive_range_end == content_range.total_size)
183187
{
184188
fetched_all = true;
185189
}
@@ -188,6 +192,7 @@ namespace snapshots
188192
// Advance range for next request
189193
range_start = range_end;
190194
range_end = range_start + range_size;
195+
inclusive_range_end = range_end - 1;
191196
}
192197
};
193198

@@ -203,7 +208,8 @@ namespace snapshots
203208

204209
ccf::curl::UniqueSlist headers;
205210
headers.append(
206-
"Range", fmt::format("bytes={}-{}", range_start, range_end));
211+
ccf::http::headers::RANGE,
212+
fmt::format("bytes={}-{}", range_start, inclusive_range_end));
207213

208214
CURLcode curl_response = CURLE_FAILED_INIT;
209215
long status_code = 0;
@@ -281,7 +287,8 @@ namespace snapshots
281287
{
282288
ccf::curl::UniqueSlist headers;
283289
headers.append(
284-
"Range", fmt::format("bytes={}-{}", range_start, range_end));
290+
ccf::http::headers::RANGE,
291+
fmt::format("bytes={}-{}", range_start, inclusive_range_end));
285292

286293
std::unique_ptr<ccf::curl::CurlRequest> snapshot_range_request;
287294
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)