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:
+56
-98
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user