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::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use reqwest::{RequestBuilder, StatusCode};
|
use reqwest::{RequestBuilder, Response, StatusCode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::compat::{self, Compatibility, ServerInfo};
|
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.
|
/// 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
|
/// `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,
|
"name": device_name,
|
||||||
});
|
});
|
||||||
let request = prepare(http()?.post(device_login_url(base_url)), None).json(&body);
|
let request = prepare(http()?.post(device_login_url(base_url)), None).json(&body);
|
||||||
let response = request
|
let refused = "That email and password didn't match an account on this server.";
|
||||||
.send()
|
let response = send(base_url, request, refused).await?;
|
||||||
.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 parsed: DeviceLoginResponse = response
|
let parsed: DeviceLoginResponse = response
|
||||||
.json()
|
.json()
|
||||||
@@ -238,20 +268,9 @@ pub async fn device_login(
|
|||||||
/// far from the thing that caused it.
|
/// far from the thing that caused it.
|
||||||
pub async fn fetch_identity(base_url: &str, token: &str) -> Result<Identity, String> {
|
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 request = prepare(http()?.get(me_url(base_url)), Some(token));
|
||||||
let response = request
|
let refused = "That token isn't valid on this server — it may have been revoked. \
|
||||||
.send()
|
Issue a new one from the web app under Account → Linked devices.";
|
||||||
.await
|
let response = send(base_url, request, refused).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));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
.json()
|
.json()
|
||||||
@@ -277,18 +296,7 @@ pub async fn fetch_changes(
|
|||||||
format!("{base_url}/api/sync/changes?since={since}")
|
format!("{base_url}/api/sync/changes?since={since}")
|
||||||
};
|
};
|
||||||
let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token));
|
let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token));
|
||||||
let response = request
|
let response = send(base_url, request, TOKEN_REJECTED).await?;
|
||||||
.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));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
.json()
|
.json()
|
||||||
@@ -308,18 +316,7 @@ pub async fn fetch_attachment(
|
|||||||
) -> Result<Vec<u8>, String> {
|
) -> Result<Vec<u8>, String> {
|
||||||
let url = format!("{base_url}/api/notes/{note_id}/attachments/{attachment_id}");
|
let url = format!("{base_url}/api/notes/{note_id}/attachments/{attachment_id}");
|
||||||
let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token));
|
let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token));
|
||||||
let response = request
|
let response = send(base_url, request, TOKEN_REJECTED).await?;
|
||||||
.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));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
.bytes()
|
.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
|
// 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.
|
// what a person can act on, and a bare status code is not.
|
||||||
let reason = response
|
let reason = server_reason(response)
|
||||||
.json::<serde_json::Value>()
|
|
||||||
.await
|
.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()));
|
.unwrap_or_else(|| format!("HTTP {}", status.as_u16()));
|
||||||
if status.is_client_error() && status != StatusCode::NOT_FOUND {
|
if status.is_client_error() && status != StatusCode::NOT_FOUND {
|
||||||
Err(UploadError::Refused(reason))
|
Err(UploadError::Refused(reason))
|
||||||
@@ -406,18 +400,7 @@ pub async fn push_changes<T: Serialize>(
|
|||||||
let body = serde_json::json!({ "changes": changes });
|
let body = serde_json::json!({ "changes": changes });
|
||||||
let url = format!("{base_url}/api/sync/push");
|
let url = format!("{base_url}/api/sync/push");
|
||||||
let request = prepare(http_with(SYNC_TIMEOUT)?.post(url), Some(token)).json(&body);
|
let request = prepare(http_with(SYNC_TIMEOUT)?.post(url), Some(token)).json(&body);
|
||||||
let response = request
|
let response = send(base_url, request, TOKEN_REJECTED).await?;
|
||||||
.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));
|
|
||||||
}
|
|
||||||
|
|
||||||
response
|
response
|
||||||
.text()
|
.text()
|
||||||
@@ -509,24 +492,14 @@ async fn read_reply<T: serde::de::DeserializeOwned>(
|
|||||||
base_url: &str,
|
base_url: &str,
|
||||||
request: RequestBuilder,
|
request: RequestBuilder,
|
||||||
) -> Result<T, String> {
|
) -> Result<T, String> {
|
||||||
let response = request
|
let response = send_raw(base_url, request, TOKEN_REJECTED).await?;
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| describe_transport_error(base_url, &e))?;
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status == StatusCode::UNAUTHORIZED {
|
|
||||||
return Err(TOKEN_REJECTED.to_string());
|
|
||||||
}
|
|
||||||
if status == StatusCode::NOT_FOUND {
|
if status == StatusCode::NOT_FOUND {
|
||||||
return Err(SHARE_NOT_FOUND.to_string());
|
return Err(SHARE_NOT_FOUND.to_string());
|
||||||
}
|
}
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
// The server's own words when it gave some ("choose someone to share with").
|
// The server's own words when it gave some ("choose someone to share with").
|
||||||
let reason = response
|
let reason = server_reason(response).await;
|
||||||
.json::<serde_json::Value>()
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from));
|
|
||||||
return Err(reason.unwrap_or_else(|| unexpected_status(base_url, status)));
|
return Err(reason.unwrap_or_else(|| unexpected_status(base_url, status)));
|
||||||
}
|
}
|
||||||
response
|
response
|
||||||
@@ -717,18 +690,13 @@ pub async fn fetch_client_release(
|
|||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<Option<ClientRelease>, String> {
|
) -> Result<Option<ClientRelease>, String> {
|
||||||
let url = format!("{base_url}/api/client/android");
|
let url = format!("{base_url}/api/client/android");
|
||||||
let response = prepare(http()?.get(url), Some(token))
|
let request = prepare(http()?.get(url), Some(token));
|
||||||
.send()
|
let response = send_raw(base_url, request, TOKEN_REJECTED).await?;
|
||||||
.await
|
|
||||||
.map_err(|e| describe_transport_error(base_url, &e))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status == StatusCode::NOT_FOUND {
|
if status == StatusCode::NOT_FOUND {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if status == StatusCode::UNAUTHORIZED {
|
|
||||||
return Err(TOKEN_REJECTED.to_string());
|
|
||||||
}
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(unexpected_status(base_url, status));
|
return Err(unexpected_status(base_url, status));
|
||||||
}
|
}
|
||||||
@@ -768,18 +736,8 @@ pub async fn download_client(
|
|||||||
let path = release.url.trim_start_matches('/');
|
let path = release.url.trim_start_matches('/');
|
||||||
let url = format!("{base_url}/{path}");
|
let url = format!("{base_url}/{path}");
|
||||||
|
|
||||||
let mut response = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token))
|
let request = prepare(http_with(SYNC_TIMEOUT)?.get(url), Some(token));
|
||||||
.send()
|
let mut response = send(base_url, request, TOKEN_REJECTED).await?;
|
||||||
.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 partial = dest.with_extension("part");
|
let partial = dest.with_extension("part");
|
||||||
if let Some(parent) = partial.parent() {
|
if let Some(parent) = partial.parent() {
|
||||||
|
|||||||
Reference in New Issue
Block a user