55 auto it = headers.find(ccf::http::headers::CONTENT_RANGE);
56 if (it == headers.end())
58 throw std::runtime_error(
59 "Response is missing expected content-range header");
62 auto [unit, remaining] = ccf::nonstd::split_1(it->second,
" ");
65 throw std::runtime_error(
66 "Unexpected content-range unit. Only 'bytes' is supported");
69 auto [range, total_size] = ccf::nonstd::split_1(remaining,
"/");
70 auto [range_start, range_end] = ccf::nonstd::split_1(range,
"-");
72 if (range_start.empty() || range_end.empty() || total_size.empty())
74 throw std::runtime_error(fmt::format(
75 "Unsupported content-range header format. Expected 'bytes "
76 "<begin>-<end>/<total>', received: {}",
80 ContentRangeHeader parsed_values{};
83 const auto [p, ec] = std::from_chars(
84 range_start.begin(), range_start.end(), parsed_values.range_start);
85 if (ec != std::errc())
87 throw std::runtime_error(fmt::format(
88 "Could not parse range start ({}) from content-range header: {}",
95 const auto [p, ec] = std::from_chars(
96 range_end.begin(), range_end.end(), parsed_values.range_end);
97 if (ec != std::errc())
99 throw std::runtime_error(fmt::format(
100 "Could not parse range end ({}) from content-range header: {}",
107 const auto [p, ec] = std::from_chars(
108 total_size.begin(), total_size.end(), parsed_values.total_size);
109 if (ec != std::errc())
111 throw std::runtime_error(fmt::format(
112 "Could not parse total size ({}) from content-range header: {}",
118 return parsed_values;
121 static std::optional<SnapshotResponse> try_fetch_from_peer(
122 const std::string& peer_address,
123 const std::string& path_to_peer_ca,
129 curl_easy.
set_opt(CURLOPT_CAINFO, path_to_peer_ca.c_str());
131 auto response_body = std::make_unique<ccf::curl::ResponseBody>(max_size);
138 std::string snapshot_url =
139 fmt::format(
"https://{}/node/snapshot", peer_address);
142 constexpr size_t range_size = 4L * 1024 * 1024;
143 size_t range_start = 0;
144 size_t range_end = range_size;
145 bool fetched_all =
false;
147 auto process_partial_response =
149 auto content_range = parse_content_range_header(request);
151 if (content_range.range_start != range_start)
153 throw std::runtime_error(fmt::format(
154 "Unexpected range response. Requested bytes {}-{}, received "
155 "range starting at {}",
158 content_range.range_start));
163 if (content_range.range_end > range_end)
165 throw std::runtime_error(fmt::format(
166 "Unexpected range response. Requested bytes {}-{}, received "
167 "range ending at {}",
170 content_range.range_end));
173 const auto range_size =
174 content_range.range_end - content_range.range_start;
176 "Received {}-byte chunk from {}. Now have {}/{}",
179 content_range.range_end,
180 content_range.total_size);
182 if (content_range.range_end == content_range.total_size)
189 range_start = range_end;
190 range_end = range_start + range_size;
194 const auto max_redirects = 20;
195 for (
auto redirect_count = 1; redirect_count <= max_redirects;
199 "Making snapshot discovery request {}/{} to {}",
206 "Range", fmt::format(
"bytes={}-{}", range_start, range_end));
208 CURLcode curl_response = CURLE_FAILED_INIT;
209 long status_code = 0;
210 std::unique_ptr<ccf::curl::CurlRequest> request;
212 [&curl_response, &status_code, &request](
213 std::unique_ptr<ccf::curl::CurlRequest>&& request_,
214 CURLcode curl_response_,
216 curl_response = curl_response_;
217 status_code = status_code_;
218 request = std::move(request_);
222 std::make_unique<ccf::curl::CurlRequest>(
223 std::move(curl_easy),
228 std::move(response_body),
229 std::move(response_callback)));
231 if (curl_response != CURLE_OK)
233 throw std::runtime_error(fmt::format(
234 "Error fetching snapshot redirect from {}: {} ({})",
236 curl_easy_strerror(curl_response),
240 if (status_code == HTTP_STATUS_NOT_FOUND)
246 if (status_code == HTTP_STATUS_PARTIAL_CONTENT)
248 process_partial_response(*request);
250 response_body = std::move(request->get_response_ptr());
251 curl_easy = std::move(request->get_easy_handle_ptr());
256 request, status_code, HTTP_STATUS_PERMANENT_REDIRECT);
258 char* redirect_url =
nullptr;
260 request->get_easy_handle(), CURLINFO_REDIRECT_URL, &redirect_url);
261 if (redirect_url ==
nullptr)
263 throw std::runtime_error(
264 "Redirect response found, but CURLINFO_REDIRECT_URL returned no "
269 "Snapshot fetch received redirect response with location {}",
271 snapshot_url = redirect_url;
273 response_body = std::move(request->get_response_ptr());
274 curl_easy = std::move(request->get_easy_handle_ptr());
277 response_body->buffer.clear();
284 "Range", fmt::format(
"bytes={}-{}", range_start, range_end));
286 std::unique_ptr<ccf::curl::CurlRequest> snapshot_range_request;
287 CURLcode curl_response = CURLE_OK;
288 long snapshot_range_status_code = 0;
292 std::unique_ptr<ccf::curl::CurlRequest>&& request_,
293 CURLcode curl_response_,
295 snapshot_range_request = std::move(request_);
296 curl_response = curl_response_;
297 snapshot_range_status_code = status_code_;
301 std::make_unique<ccf::curl::CurlRequest>(
302 std::move(curl_easy),
307 std::move(response_body),
308 snapshot_response_callback));
309 if (curl_response != CURLE_OK)
311 throw std::runtime_error(fmt::format(
312 "Error fetching snapshot chunk range from {}: {} ({})",
313 snapshot_range_request->get_url(),
314 curl_easy_strerror(curl_response),
315 snapshot_range_status_code));
318 snapshot_range_request,
319 snapshot_range_status_code,
320 HTTP_STATUS_PARTIAL_CONTENT);
322 process_partial_response(*snapshot_range_request);
324 response_body = std::move(snapshot_range_request->get_response_ptr());
325 curl_easy = std::move(snapshot_range_request->get_easy_handle_ptr());
328 const auto url_components = ccf::nonstd::split(snapshot_url,
"/");
329 const std::string snapshot_name(url_components.back());
331 return SnapshotResponse{snapshot_name, std::move(response_body->buffer)};
333 catch (
const std::exception& e)
335 LOG_FAIL_FMT(
"Error during snapshot fetch: {}", e.what());
340 static std::optional<SnapshotResponse> fetch_from_peer(
341 const std::string& peer_address,
342 const std::string& path_to_peer_ca,
344 size_t retry_delay_ms,
347 for (
size_t attempt = 0; attempt < max_attempts; ++attempt)
350 "Fetching snapshot from {} (attempt {}/{})",
357 std::this_thread::sleep_for(std::chrono::milliseconds(retry_delay_ms));
361 try_fetch_from_peer(peer_address, path_to_peer_ca, max_size);
362 if (response.has_value())
368 "Exceeded maximum snapshot fetch retries ({}), giving up", max_attempts);