Skip to content

Commit

Permalink
upgrade ureq to v3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
wfraser committed Feb 6, 2025
1 parent bac4981 commit ac6b94e
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 75 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ default-features = false
features = ["http2", "rustls-tls", "stream"]

[dependencies.ureq]
version = "2.5.0"
version = "3.0.4"
optional = true
default-features = false
features = ["tls"]
features = ["rustls"]

[dev-dependencies]
env_logger = "0.11"
Expand Down
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# unreleased changes
* Default non-async HTTP client, ureq, updated from major version 2 to 3.

# v0.19.0
2025-01-03
* **BIG CHANGE: async support added**
Expand Down
104 changes: 31 additions & 73 deletions src/default_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ use crate::default_client_common::impl_set_path_root;
use crate::oauth2::{Authorization, TokenCache};
use crate::Error;
use futures::FutureExt;
use std::borrow::Cow;
use std::fmt::Write;
use std::str::FromStr;
use std::sync::Arc;
use ureq::typestate::WithBody;
use ureq::Agent;

macro_rules! impl_update_token {
($self:ident) => {
Expand Down Expand Up @@ -237,13 +237,18 @@ impl crate::async_client_trait::NoauthClient for TokenUpdateClient<'_> {}

#[derive(Debug)]
struct UreqClient {
agent: ureq::Agent,
agent: Agent,
}

impl Default for UreqClient {
fn default() -> Self {
Self {
agent: ureq::Agent::new(),
agent: Agent::new_with_config(
Agent::config_builder()
.https_only(true)
.http_status_as_error(false)
.build(),
),
}
}
}
Expand All @@ -253,24 +258,33 @@ impl HttpClient for UreqClient {

fn execute(&self, request: Self::Request, body: &[u8]) -> Result<HttpRequestResultRaw, Error> {
let resp = if body.is_empty() {
request.req.call()
request.req.send_empty()
} else {
request.req.send_bytes(body)
request.req.send(body)
};

let (status, resp) = match resp {
Ok(resp) => (resp.status(), resp),
Err(ureq::Error::Status(status, resp)) => (status, resp),
Err(e @ ureq::Error::Transport(_)) => {
Ok(resp) => (resp.status().as_u16(), resp),
Err(ureq::Error::Io(e)) => {
return Err(e.into());
}
Err(e) => {
return Err(RequestError { inner: e }.into());
}
};

let result_header = resp.header("Dropbox-API-Result").map(String::from);
let result_header = resp
.headers()
.get("Dropbox-API-Result")
.map(|v| String::from_utf8(v.as_bytes().to_vec()))
.transpose()
.map_err(|e| e.utf8_error())?;

let content_length = resp
.header("Content-Length")
.map(|s| {
.headers()
.get("Content-Length")
.map(|v| {
let s = std::str::from_utf8(v.as_bytes())?;
u64::from_str(s).map_err(|e| {
Error::UnexpectedResponse(format!("invalid Content-Length {s:?}: {e}"))
})
Expand All @@ -281,7 +295,7 @@ impl HttpClient for UreqClient {
status,
result_header,
content_length,
body: resp.into_reader(),
body: Box::new(resp.into_body().into_reader()),
})
}

Expand All @@ -294,18 +308,12 @@ impl HttpClient for UreqClient {

/// This is an implementation detail of the HTTP client.
pub struct UreqRequest {
req: ureq::Request,
req: ureq::RequestBuilder<WithBody>,
}

impl HttpRequest for UreqRequest {
fn set_header(mut self, name: &str, value: &str) -> Self {
if name.eq_ignore_ascii_case("dropbox-api-arg") {
// Non-ASCII and 0x7F in a header need to be escaped per the HTTP spec, and ureq doesn't
// do this for us. This is only an issue for this particular header.
self.req = self.req.set(name, json_escape_header(value).as_ref());
} else {
self.req = self.req.set(name, value);
}
self.req = self.req.header(name, value);
self
}
}
Expand All @@ -316,7 +324,7 @@ impl HttpRequest for UreqRequest {
pub enum DefaultClientError {
/// The HTTP client encountered invalid UTF-8 data.
#[error("invalid UTF-8 string")]
Utf8(#[from] std::string::FromUtf8Error),
Utf8(#[from] std::str::Utf8Error),

/// The HTTP client encountered some I/O error.
#[error("I/O error: {0}")]
Expand All @@ -339,7 +347,7 @@ macro_rules! wrap_error {
}

wrap_error!(std::io::Error);
wrap_error!(std::string::FromUtf8Error);
wrap_error!(std::str::Utf8Error);
wrap_error!(RequestError);

/// Something went wrong making the request, or the server returned a response we didn't expect.
Expand Down Expand Up @@ -367,53 +375,3 @@ impl std::error::Error for RequestError {
Some(&self.inner)
}
}

/// Replaces any non-ASCII characters (and 0x7f) with JSON-style '\uXXXX' sequence. Otherwise,
/// returns it unmodified without any additional allocation or copying.
fn json_escape_header(s: &str) -> Cow<'_, str> {
// Unfortunately, the HTTP spec requires escaping ASCII DEL (0x7F), so we can't use the quicker
// bit pattern check done in str::is_ascii() to skip this for the common case of all ASCII. :(

let mut out = Cow::Borrowed(s);
for (i, c) in s.char_indices() {
if !c.is_ascii() || c == '\x7f' {
let mstr = match out {
Cow::Borrowed(_) => {
// If we're still borrowed, we must have had ascii up until this point.
// Clone the string up until here, and from now on we'll be pushing chars to it.
out = Cow::Owned(s[0..i].to_owned());
out.to_mut()
}
Cow::Owned(ref mut m) => m,
};
write!(mstr, "\\u{:04x}", c as u32).unwrap();
} else if let Cow::Owned(ref mut o) = out {
o.push(c);
}
}
out
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_json_escape() {
assert_eq!(Cow::Borrowed("foobar"), json_escape_header("foobar"));
assert_eq!(
Cow::<'_, str>::Owned("tro\\u0161kovi".to_owned()),
json_escape_header("troškovi")
);
assert_eq!(
Cow::<'_, str>::Owned(
r#"{"field": "some_\u00fc\u00f1\u00eec\u00f8d\u00e9_and_\u007f"}"#.to_owned()
),
json_escape_header("{\"field\": \"some_üñîcødé_and_\x7f\"}")
);
assert_eq!(
Cow::<'_, str>::Owned("almost,\\u007f but not quite".to_owned()),
json_escape_header("almost,\x7f but not quite")
);
}
}

0 comments on commit ac6b94e

Please sign in to comment.