From 4ac15d35579ba00541653defaa466b1e1a5fbe25 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 8 Oct 2026 14:19:38 -0400 Subject: [PATCH] Every call to the server fails the same way through client::send Seven calls each spelled out send, describe the transport error, map a 401, and refuse anything else as unexpected_status; two read the server's {"error"} words the same way. send_raw (transport + a 401 whose meaning the caller names), send (and anything but success is unexpected) and server_reason now hold those steps. Each call keeps only what is its own: device_login's and fetch_identity's 401 wording, the release's 404 = none, sharing's 404 and refusal reason, upload's retry split. DRY pass #2, batch 2, F6 (#5372). Co-Authored-By: Claude Opus 5.5 --- core/src/sync/client.rs | 154 +++++++++++++++------------------------- 1 file changed, 56 insertions(+), 98 deletions(-) diff --git a/core/src/sync/client.rs b/core/src/sync/client.rs index f7a89b1..949fb69 100644 --- a/core/src/sync/client.rs +++ b/core/src/sync/client.rs @@ -11,7 +11,7 @@ use std::path::Path; use std::time::Duration; -use reqwest::{RequestBuilder, StatusCode}; +use reqwest::{RequestBuilder, Response, StatusCode}; use serde::{Deserialize, Serialize}; use super::compat::{self, Compatibility, ServerInfo}; @@ -154,6 +154,46 @@ fn unexpected_status(base_url: &str, status: StatusCode) -> String { ) } +/// Send `request`, failing the way every call to the server fails alike: a +/// transport error, described for a person, and a 401, as `unauthorized` (what a +/// rejected credential means depends on the call). Any other status is the +/// caller's to read; [`send`] is this plus "anything but success is unexpected". +async fn send_raw( + base_url: &str, + request: RequestBuilder, + unauthorized: &str, +) -> Result { + let response = request + .send() + .await + .map_err(|e| describe_transport_error(base_url, &e))?; + if response.status() == StatusCode::UNAUTHORIZED { + return Err(unauthorized.to_string()); + } + Ok(response) +} + +/// [`send_raw`], and anything but a success is [`unexpected_status`]. +async fn send( + base_url: &str, + request: RequestBuilder, + unauthorized: &str, +) -> Result { + let response = send_raw(base_url, request, unauthorized).await?; + let status = response.status(); + if !status.is_success() { + return Err(unexpected_status(base_url, status)); + } + Ok(response) +} + +/// The server's own words for a refusal, when it gave some (`{"error": "…"}`). +/// "file is too large (max 25 MB)" is what a person can act on; a status is not. +async fn server_reason(response: Response) -> Option { + let body = response.json::().await.ok()?; + body.get("error")?.as_str().map(String::from) +} + /// Ask a server who it is and whether we can sync with it. /// /// `Err` means we never got a usable answer (bad address, unreachable, not an @@ -211,18 +251,8 @@ pub async fn device_login( "name": device_name, }); let request = prepare(http()?.post(device_login_url(base_url)), None).json(&body); - let response = request - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; - - let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - return Err("That email and password didn't match an account on this server.".to_string()); - } - if !status.is_success() { - return Err(unexpected_status(base_url, status)); - } + let refused = "That email and password didn't match an account on this server."; + let response = send(base_url, request, refused).await?; let parsed: DeviceLoginResponse = response .json() @@ -238,20 +268,9 @@ pub async fn device_login( /// far from the thing that caused it. pub async fn fetch_identity(base_url: &str, token: &str) -> Result { let request = prepare(http()?.get(me_url(base_url)), Some(token)); - let response = request - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; - - let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - let message = "That token isn't valid on this server — it may have been revoked. \ - Issue a new one from the web app under Account → Linked devices."; - return Err(message.to_string()); - } - if !status.is_success() { - return Err(unexpected_status(base_url, status)); - } + let refused = "That token isn't valid on this server — it may have been revoked. \ + Issue a new one from the web app under Account → Linked devices."; + let response = send(base_url, request, refused).await?; response .json() @@ -277,18 +296,7 @@ pub async fn fetch_changes( format!("{base_url}/api/sync/changes?since={since}") }; let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token)); - let response = request - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; - - let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - return Err(TOKEN_REJECTED.to_string()); - } - if !status.is_success() { - return Err(unexpected_status(base_url, status)); - } + let response = send(base_url, request, TOKEN_REJECTED).await?; response .json() @@ -308,18 +316,7 @@ pub async fn fetch_attachment( ) -> Result, String> { let url = format!("{base_url}/api/notes/{note_id}/attachments/{attachment_id}"); let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token)); - let response = request - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; - - let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - return Err(TOKEN_REJECTED.to_string()); - } - if !status.is_success() { - return Err(unexpected_status(base_url, status)); - } + let response = send(base_url, request, TOKEN_REJECTED).await?; response .bytes() @@ -378,11 +375,8 @@ pub async fn upload_attachment( } // The server's own words, when it gave some: "file is too large (max 25 MB)" is // what a person can act on, and a bare status code is not. - let reason = response - .json::() + let reason = server_reason(response) .await - .ok() - .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from)) .unwrap_or_else(|| format!("HTTP {}", status.as_u16())); if status.is_client_error() && status != StatusCode::NOT_FOUND { Err(UploadError::Refused(reason)) @@ -406,18 +400,7 @@ pub async fn push_changes( let body = serde_json::json!({ "changes": changes }); let url = format!("{base_url}/api/sync/push"); let request = prepare(http_with(SYNC_TIMEOUT)?.post(url), Some(token)).json(&body); - let response = request - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; - - let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - return Err(TOKEN_REJECTED.to_string()); - } - if !status.is_success() { - return Err(unexpected_status(base_url, status)); - } + let response = send(base_url, request, TOKEN_REJECTED).await?; response .text() @@ -509,24 +492,14 @@ async fn read_reply( base_url: &str, request: RequestBuilder, ) -> Result { - let response = request - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; + let response = send_raw(base_url, request, TOKEN_REJECTED).await?; let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - return Err(TOKEN_REJECTED.to_string()); - } if status == StatusCode::NOT_FOUND { return Err(SHARE_NOT_FOUND.to_string()); } if !status.is_success() { // The server's own words when it gave some ("choose someone to share with"). - let reason = response - .json::() - .await - .ok() - .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from)); + let reason = server_reason(response).await; return Err(reason.unwrap_or_else(|| unexpected_status(base_url, status))); } response @@ -717,18 +690,13 @@ pub async fn fetch_client_release( token: &str, ) -> Result, String> { let url = format!("{base_url}/api/client/android"); - let response = prepare(http()?.get(url), Some(token)) - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; + let request = prepare(http()?.get(url), Some(token)); + let response = send_raw(base_url, request, TOKEN_REJECTED).await?; let status = response.status(); if status == StatusCode::NOT_FOUND { return Ok(None); } - if status == StatusCode::UNAUTHORIZED { - return Err(TOKEN_REJECTED.to_string()); - } if !status.is_success() { return Err(unexpected_status(base_url, status)); } @@ -768,18 +736,8 @@ pub async fn download_client( let path = release.url.trim_start_matches('/'); let url = format!("{base_url}/{path}"); - let mut response = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token)) - .send() - .await - .map_err(|e| describe_transport_error(base_url, &e))?; - - let status = response.status(); - if status == StatusCode::UNAUTHORIZED { - return Err(TOKEN_REJECTED.to_string()); - } - if !status.is_success() { - return Err(unexpected_status(base_url, status)); - } + let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token)); + let mut response = send(base_url, request, TOKEN_REJECTED).await?; let partial = dest.with_extension("part"); if let Some(parent) = partial.parent() {