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 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:19:38 -04:00
co-authored by Claude Opus 5.5
parent 0101b05487
commit 4ac15d3557
+56 -98
View File
@@ -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<Response, String> {
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<Response, String> {
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<String> {
let body = response.json::<serde_json::Value>().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<Identity, String> {
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<Vec<u8>, 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::<serde_json::Value>()
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<T: Serialize>(
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<T: serde::de::DeserializeOwned>(
base_url: &str,
request: RequestBuilder,
) -> Result<T, String> {
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::<serde_json::Value>()
.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<Option<ClientRelease>, 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() {