Skip to main content

max / supernote-push

Upgrade ureq to 3.3 ureq 2 -> 3.3.0. The 3.x rework touches every call in browse.rs: - AgentBuilder::new().timeout(d) becomes Agent::config_builder() .timeout_global(Some(d)), built through Agent::new_with_config. - Request::set(k, v) becomes header(k, v); send_bytes(&[u8]) becomes send(&[u8]); Response::into_string() becomes body_mut().read_to_string(), which keeps the same 10MB cap and now converts lossily instead of failing on stray bytes. - Error handling changed shape. ureq 2 surfaced 4xx/5xx as Error::Status(code, response), which is where Error::Status got its body. ureq 3's Error::StatusCode carries the code alone. The agent therefore sets http_status_as_error(false) and the new browse::status_error checks the status where the response, and so the body, is still in hand. probe() collapses accordingly: with statuses no longer split across Ok and Err, classify_probe_status holds the mapping, with a test pinning the rule that a 401/403 auth challenge still counts as a reachable device. Also drops the "json" feature, which nothing used; listing JSON is parsed with serde_json directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 21:27 UTC
Commit: 43e42af037d72978d2db116a0d82c7ad1e3efbb3
Parent: cb11d00
3 files changed, +66 insertions, -18 deletions
M Cargo.toml +1 -1
@@ -9,7 +9,7 @@ keywords = ["supernote", "eink", "ratta"]
9 9 categories = ["api-bindings", "network-programming"]
10 10
11 11 [dependencies]
12 - ureq = { version = "2", features = ["json"] }
12 + ureq = "3.3"
13 13 mdns-sd = "0.11"
14 14 serde = { version = "1", features = ["derive"] }
15 15 serde_json = "1"
M src/browse.rs +59 -12
@@ -31,11 +31,19 @@ const PATH: &AsciiSet = &CONTROLS
31 31 pub(crate) fn probe(host: &str, port: u16, timeout: Duration) -> Reachability {
32 32 let url = format!("http://{host}:{port}/");
33 33 match agent(timeout).get(&url).call() {
34 - Ok(resp) if resp.status() >= 200 && resp.status() < 300 => Reachability::Up,
35 - Ok(_) => Reachability::WifiTransferOff,
36 - Err(ureq::Error::Status(401 | 403, _)) => Reachability::Up,
37 - Err(ureq::Error::Status(_, _)) => Reachability::WifiTransferOff,
38 - Err(ureq::Error::Transport(_)) => Reachability::Unreachable,
34 + // The agent reports 4xx/5xx as ordinary responses, so every status
35 + // lands in this arm rather than being split across Ok and Err.
36 + Ok(resp) => classify_probe_status(resp.status().as_u16()),
37 + Err(_) => Reachability::Unreachable,
38 + }
39 + }
40 +
41 + /// A passcode-protected device still answers, it just answers 401/403, so an
42 + /// auth challenge counts as up.
43 + fn classify_probe_status(status: u16) -> Reachability {
44 + match status {
45 + 200..=299 | 401 | 403 => Reachability::Up,
46 + _ => Reachability::WifiTransferOff,
39 47 }
40 48 }
41 49
@@ -53,12 +61,15 @@ pub(crate) fn upload(
53 61
54 62 let mut req = agent(timeout)
55 63 .post(&url)
56 - .set("Content-Type", &content_type)
57 - .set("file-size", &bytes.len().to_string());
64 + .header("Content-Type", &content_type)
65 + .header("file-size", bytes.len().to_string());
58 66 if let Some(pc) = passcode {
59 - req = req.set("Authorization", &basic(pc));
67 + req = req.header("Authorization", basic(pc));
68 + }
69 + let mut resp = req.send(&body[..]).map_err(Error::from_http)?;
70 + if let Some(e) = status_error(&mut resp) {
71 + return Err(e);
60 72 }
61 - req.send_bytes(&body).map_err(Error::from_http)?;
62 73 Ok(())
63 74 }
64 75
@@ -72,9 +83,13 @@ pub(crate) fn list(
72 83 let url = build_url(host, port, dir, None);
73 84 let mut req = agent(timeout).get(&url);
74 85 if let Some(pc) = passcode {
75 - req = req.set("Authorization", &basic(pc));
86 + req = req.header("Authorization", basic(pc));
87 + }
88 + let mut resp = req.call().map_err(Error::from_http)?;
89 + if let Some(e) = status_error(&mut resp) {
90 + return Err(e);
76 91 }
77 - let html = req.call().map_err(Error::from_http)?.into_string()?;
92 + let html = resp.body_mut().read_to_string().map_err(Error::from_http)?;
78 93 let json = extract_embedded_json(&html).ok_or_else(|| {
79 94 Error::Http("could not find embedded webInfo JSON in listing page".to_string())
80 95 })?;
@@ -88,7 +103,28 @@ pub(crate) fn list(
88 103 }
89 104
90 105 fn agent(timeout: Duration) -> ureq::Agent {
91 - ureq::AgentBuilder::new().timeout(timeout).build()
106 + ureq::Agent::new_with_config(
107 + ureq::Agent::config_builder()
108 + .timeout_global(Some(timeout))
109 + // ureq 3 would otherwise fail a 4xx/5xx with Error::StatusCode,
110 + // which carries the code but not the response. The device puts a
111 + // useful message in the body, so take every status as a normal
112 + // response and inspect it in `status_error`.
113 + .http_status_as_error(false)
114 + .build(),
115 + )
116 + }
117 +
118 + /// Returns the error for a non-2xx response, draining the body into it.
119 + fn status_error(resp: &mut ureq::http::Response<ureq::Body>) -> Option<Error> {
120 + let status = resp.status().as_u16();
121 + if (200..300).contains(&status) {
122 + return None;
123 + }
124 + Some(Error::Status {
125 + status,
126 + body: resp.body_mut().read_to_string().unwrap_or_default(),
127 + })
92 128 }
93 129
94 130 fn build_url(host: &str, port: u16, dir: &str, filename: Option<&str>) -> String {
@@ -269,6 +305,17 @@ mod tests {
269 305 }
270 306
271 307 #[test]
308 + fn probe_treats_auth_challenges_as_reachable() {
309 + assert_eq!(classify_probe_status(200), Reachability::Up);
310 + assert_eq!(classify_probe_status(204), Reachability::Up);
311 + assert_eq!(classify_probe_status(401), Reachability::Up);
312 + assert_eq!(classify_probe_status(403), Reachability::Up);
313 + // Anything else answering on the port is a device with Wi-Fi Transfer off.
314 + assert_eq!(classify_probe_status(404), Reachability::WifiTransferOff);
315 + assert_eq!(classify_probe_status(500), Reachability::WifiTransferOff);
316 + }
317 +
318 + #[test]
272 319 fn basic_header_is_colon_prefixed() {
273 320 assert_eq!(basic("secret"), "Basic OnNlY3JldA==");
274 321 }
M src/error.rs +6 -5
@@ -13,13 +13,14 @@ pub enum Error {
13 13 }
14 14
15 15 impl Error {
16 + /// `Status` is not produced here. ureq 3's `Error::StatusCode` carries only
17 + /// the code, never the response, so the agent turns off
18 + /// `http_status_as_error` and `browse` checks the status while it still
19 + /// holds the body. See `browse::status_error`.
16 20 pub(crate) fn from_http(e: ureq::Error) -> Self {
17 21 match e {
18 - ureq::Error::Status(status, resp) => Self::Status {
19 - status,
20 - body: resp.into_string().unwrap_or_default(),
21 - },
22 - ureq::Error::Transport(t) => Self::Http(t.to_string()),
22 + ureq::Error::Io(io) => Self::Io(io),
23 + other => Self::Http(other.to_string()),
23 24 }
24 25 }
25 26