Skip to content

Commit 7f79566

Browse files
authored
Update network (grid download) functionality to use ureq 3.x (#228)
- [x] I agree to follow the project's [code of conduct](https://github.com/georust/.github/blob/main/CODE_OF_CONDUCT.md). - [x] I added an entry to the project's change log file if knowledge of this change could be valuable to users. - Usually called `CHANGES.md` or `CHANGELOG.md` - Prefix changelog entries for breaking changes with "BREAKING: " --- <s>No changelog entry yet since we have other PRs in flight.</s> This was an unfun piece of work as the ureq API has evolved significantly between 2.x and 3.0, and the way we're passing the connection pool object around is more difficult to do now. That having been said the tests pass and none of the changes affect the public API. We also have a new optional dep in the form of `http`, but I suspect it was present previously anyway.
2 parents c49fbdf + 70e25c2 commit 7f79566

File tree

3 files changed

+104
-55
lines changed

3 files changed

+104
-55
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# UNRELEASED
22
- Add coordinate metadata creation and query functions
33
- Add method for Proj creation from existing Proj instances, optionally containing epochs
4+
- Update ureq to 3.x and adapt network functionality to its new API
45

56
## 0.29.0 - 2025-03-17
67

Cargo.toml

+4-3
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ rust-version = "1.82"
1717
[dependencies]
1818
proj-sys = { version = "0.25.0", path = "proj-sys" }
1919
geo-types = { version = "0.7.10", optional = true }
20-
libc = "0.2.119"
20+
libc = "0.2.172"
2121
num-traits = "0.2.14"
2222
thiserror = "2.0.0"
23-
ureq = { version = "2.0.0", optional = true }
23+
ureq = { version = "3.0.11", optional = true }
24+
http = { version = "1.3.0", optional = true }
2425

2526
[workspace]
2627
members = ["proj-sys"]
@@ -29,7 +30,7 @@ members = ["proj-sys"]
2930
default = ["geo-types"]
3031
bundled_proj = [ "proj-sys/bundled_proj" ]
3132
pkg_config = [ "proj-sys/pkg_config" ]
32-
network = ["ureq", "proj-sys/network"]
33+
network = ["ureq", "http", "proj-sys/network"]
3334

3435
[dev-dependencies]
3536
# approx version must match the one used in geo-types

src/network.rs

+99-52
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::io::Read;
2020
use std::ops::Range;
2121
use std::os::raw::c_ulonglong;
2222
use std::ptr::{self, NonNull};
23-
use ureq::{Agent, Request, Response};
23+
use ureq::Agent;
2424

2525
use crate::proj::{ProjError, _string};
2626
use libc::c_char;
@@ -63,10 +63,10 @@ impl Drop for HandleData {
6363
}
6464
}
6565

66-
/// Return an exponential wait time based on the number of retries
66+
/// Return a quadratically-increasing wait time based on the number of retries
6767
///
6868
/// Example: a value of 8 allows up to 6400 ms of retry delay, for a cumulative total of 25500 ms
69-
fn get_wait_time_exp(retrycount: i32) -> u64 {
69+
fn get_wait_time_quad(retrycount: i32) -> u64 {
7070
if retrycount == 0 {
7171
return 0;
7272
}
@@ -75,34 +75,46 @@ fn get_wait_time_exp(retrycount: i32) -> u64 {
7575

7676
/// Process CDN response: handle retries in case of server error, or early return for client errors
7777
/// Successful retry data is stored into res
78-
fn error_handler(res: &mut Response, rb: Request) -> Result<&Response, ProjError> {
78+
fn error_handler<'a>(
79+
res: &'a mut http::Response<ureq::Body>,
80+
url: &str,
81+
headers: &[(&str, &str)],
82+
clt: Agent,
83+
) -> Result<&'a mut http::Response<ureq::Body>, ProjError> {
7984
let mut retries = 0;
8085
// Check whether something went wrong on the server, or if it's an S3 retry code
81-
if SERVER_ERROR_CODES.contains(&res.status()) || RETRY_CODES.contains(&res.status()) {
86+
if SERVER_ERROR_CODES.contains(&res.status().as_u16())
87+
|| RETRY_CODES.contains(&res.status().as_u16())
88+
{
8289
// Start retrying: up to MAX_RETRIES
83-
while (SERVER_ERROR_CODES.contains(&res.status()) || RETRY_CODES.contains(&res.status()))
90+
while (SERVER_ERROR_CODES.contains(&res.status().as_u16())
91+
|| RETRY_CODES.contains(&res.status().as_u16()))
8492
&& retries <= MAX_RETRIES
8593
{
8694
retries += 1;
87-
let wait = time::Duration::from_millis(get_wait_time_exp(retries as i32));
95+
let wait = time::Duration::from_millis(get_wait_time_quad(retries as i32));
8896
thread::sleep(wait);
89-
let retry = rb.clone();
90-
*res = retry.call()?;
97+
let mut req = clt.get(url);
98+
// Apply all headers
99+
for (name, value) in headers {
100+
req = req.header(*name, *value);
101+
}
102+
*res = req.call()?;
91103
}
92104
// Not a timeout or known S3 retry code: bail out
93-
} else if CLIENT_ERROR_CODES.contains(&res.status()) {
105+
} else if CLIENT_ERROR_CODES.contains(&res.status().as_u16()) {
94106
return Err(ProjError::DownloadError(
95-
res.status_text().to_string(),
96-
res.get_url().to_string(),
107+
res.status().to_string(),
108+
url.to_string(),
97109
retries,
98110
));
99111
}
100112
// Retries have been exhausted OR
101113
// The loop ended prematurely due to a different error
102-
if !SUCCESS_ERROR_CODES.contains(&res.status()) {
114+
if !SUCCESS_ERROR_CODES.contains(&res.status().as_u16()) {
103115
return Err(ProjError::DownloadError(
104-
res.status_text().to_string(),
105-
res.get_url().to_string(),
116+
res.status().to_string(),
117+
url.to_string(),
106118
retries,
107119
));
108120
}
@@ -148,7 +160,7 @@ pub(crate) unsafe extern "C" fn network_open(
148160
let err_string = e.to_string();
149161
out_error_string.copy_from_nonoverlapping(err_string.as_ptr().cast(), err_string.len());
150162
out_error_string.add(err_string.len()).write(0);
151-
ptr::null_mut() as *mut PROJ_NETWORK_HANDLE
163+
ptr::null_mut()
152164
}
153165
}
154166
}
@@ -172,44 +184,62 @@ unsafe fn _network_open(
172184
// RANGE header definition is "bytes=x-y"
173185
let hvalue = format!("bytes={offset}-{end}");
174186
// Create a new client that can be reused for subsequent queries
175-
let clt = Agent::new();
176-
let req = clt.get(&url);
177-
let with_headers = req.set("Range", &hvalue).set("Client", CLIENT);
178-
let in_case_of_error = with_headers.clone();
179-
let mut res = with_headers.call()?;
187+
let clt = Agent::new_with_defaults();
188+
let req = clt
189+
.get(&url)
190+
.header("Range", &hvalue)
191+
.header("Client", CLIENT);
192+
193+
let mut res = req.call()?;
194+
195+
// Define headers for potential retries
196+
let headers = [("Range", hvalue.as_str()), ("Client", CLIENT)];
197+
180198
// hand the response off to the error-handler, continue on success
181-
error_handler(&mut res, in_case_of_error)?;
199+
error_handler(&mut res, &url, &headers, clt.clone())?;
200+
182201
// Write the initial read length value into the pointer
183-
let Some(Ok(contentlength)) = res.header("Content-Length").map(str::parse::<usize>) else {
184-
return Err(ProjError::ContentLength);
185-
};
202+
let contentlength = res
203+
.headers()
204+
.get("Content-Length")
205+
.and_then(|val| val.to_str().ok())
206+
.and_then(|s| s.parse::<usize>().ok())
207+
.ok_or(ProjError::ContentLength)?;
208+
186209
let headers = res
187-
.headers_names()
188-
.into_iter()
189-
.filter_map(|h| {
190-
Some({
191-
let v = res.header(&h)?.to_string();
192-
(h, v)
193-
})
210+
.headers()
211+
.iter()
212+
.filter_map(|(h, v)| {
213+
let header_name = h.to_string();
214+
let header_value = v.to_str().ok()?.to_string();
215+
Some((header_name, header_value))
194216
})
195217
.collect();
218+
196219
// Copy the downloaded bytes into the buffer so it can be passed around
197220
let capacity = contentlength.min(size_to_read);
198-
let mut buf = Vec::with_capacity(capacity);
199-
res.into_reader()
221+
let mut buf = Vec::<u8>::with_capacity(capacity);
222+
223+
// Read from body into our buffer
224+
let body_reader = res.body_mut().as_reader();
225+
body_reader
200226
.take(size_to_read as u64)
201227
.read_to_end(&mut buf)?;
228+
202229
out_size_read.write(buf.len());
203230
buf.as_ptr().copy_to_nonoverlapping(buffer.cast(), capacity);
231+
204232
let hd = HandleData::new(url, headers, None);
205233
// heap-allocate the struct and cast it to a void pointer so it can be passed around to PROJ
206234
let hd_boxed = Box::new(hd);
207235
let void: *mut c_void = Box::into_raw(hd_boxed).cast::<libc::c_void>();
208236
let opaque: *mut PROJ_NETWORK_HANDLE = void.cast::<proj_sys::PROJ_NETWORK_HANDLE>();
237+
209238
// If everything's OK, set the error string to empty
210239
let err_string = "";
211240
out_error_string.copy_from_nonoverlapping(err_string.as_ptr().cast(), err_string.len());
212241
out_error_string.add(err_string.len()).write(0);
242+
213243
Ok(opaque)
214244
}
215245

@@ -334,41 +364,58 @@ fn _network_read_range(
334364
let end = offset as usize + size_to_read - 1;
335365
let hvalue = format!("bytes={offset}-{end}");
336366
let hd = unsafe { &mut *(handle as *const c_void as *mut HandleData) };
337-
let clt = Agent::new();
338-
let initial = clt.get(&hd.url);
339-
let in_case_of_error = initial.clone().set("Range", &hvalue).set("Client", CLIENT);
340-
let req = in_case_of_error.clone();
367+
let clt = Agent::new_with_defaults();
368+
let req = clt
369+
.get(&hd.url)
370+
.header("Range", &hvalue)
371+
.header("Client", CLIENT);
372+
341373
let mut res = req.call()?;
342-
// hand the response and retry instance off to the error-handler, continue on success
343-
error_handler(&mut res, in_case_of_error)?;
374+
375+
// Define headers for potential retries
376+
let headers = [("Range", hvalue.as_str()), ("Client", CLIENT)];
377+
378+
// hand the response off to the error-handler, continue on success
379+
error_handler(&mut res, &hd.url, &headers, clt.clone())?;
380+
344381
let headers = res
345-
.headers_names()
346-
.into_iter()
347-
.filter_map(|h| {
348-
Some({
349-
let v = res.header(&h)?.to_string();
350-
(h, v)
351-
})
382+
.headers()
383+
.iter()
384+
.filter_map(|(h, v)| {
385+
let header_name = h.to_string();
386+
let header_value = v.to_str().ok()?.to_string();
387+
Some((header_name, header_value))
352388
})
353389
.collect();
354-
let Some(Ok(contentlength)) = res.header("Content-Length").map(str::parse::<usize>) else {
355-
return Err(ProjError::ContentLength);
356-
};
390+
391+
let contentlength = res
392+
.headers()
393+
.get("Content-Length")
394+
.and_then(|val| val.to_str().ok())
395+
.and_then(|s| s.parse::<usize>().ok())
396+
.ok_or(ProjError::ContentLength)?;
397+
357398
// Copy the downloaded bytes into the buffer so it can be passed around
358399
let capacity = contentlength.min(size_to_read);
359-
let mut buf = Vec::with_capacity(capacity);
360-
res.into_reader()
400+
let mut buf = Vec::<u8>::with_capacity(capacity);
401+
402+
// Read from body into our buffer
403+
let body_reader = res.body_mut().as_reader();
404+
body_reader
361405
.take(size_to_read as u64)
362406
.read_to_end(&mut buf)?;
407+
363408
unsafe {
364409
buf.as_ptr()
365410
.copy_to_nonoverlapping(buffer.cast::<u8>(), capacity);
366411
}
412+
367413
let err_string = "";
368414
unsafe {
369415
out_error_string.copy_from_nonoverlapping(err_string.as_ptr().cast(), err_string.len());
370416
out_error_string.add(err_string.len()).write(0);
371417
}
418+
372419
hd.headers = headers;
373420
Ok(buf.len())
374421
}

0 commit comments

Comments
 (0)