aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client/src/api_client.rs
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2025-05-21 17:36:23 -0700
committerGitHub <noreply@github.com>2025-05-21 17:36:23 -0700
commit87a963600cb6bea8f594d53f3a19920af7560169 (patch)
tree99fcec203cfd1a405dc3528e07255352da307cee /crates/atuin-client/src/api_client.rs
parentFormatting (diff)
downloadatuin-87a963600cb6bea8f594d53f3a19920af7560169.zip
fix(api): Allow trailing slashes in sync_address (#2760)
Diffstat (limited to 'crates/atuin-client/src/api_client.rs')
-rw-r--r--crates/atuin-client/src/api_client.rs76
1 files changed, 50 insertions, 26 deletions
diff --git a/crates/atuin-client/src/api_client.rs b/crates/atuin-client/src/api_client.rs
index 0bd16c50..c2bdfadc 100644
--- a/crates/atuin-client/src/api_client.rs
+++ b/crates/atuin-client/src/api_client.rs
@@ -2,7 +2,7 @@ use std::collections::HashMap;
use std::env;
use std::time::Duration;
-use eyre::{Result, bail};
+use eyre::{Result, bail, eyre};
use reqwest::{
Response, StatusCode, Url,
header::{AUTHORIZATION, HeaderMap, USER_AGENT},
@@ -35,6 +35,25 @@ pub struct Client<'a> {
client: reqwest::Client,
}
+fn make_url(address: &str, path: &str) -> Result<String> {
+ // `join()` expects a trailing `/` in order to join paths
+ // e.g. it treats `http://host:port/subdir` as a file called `subdir`
+ let address = if address.ends_with("/") {
+ address
+ } else {
+ &format!("{}/", address)
+ };
+
+ // passing a path with a leading `/` will cause `join()` to replace the entire URL path
+ let path = path.strip_prefix("/").unwrap_or(path);
+
+ let url = Url::parse(address)
+ .map(|url| url.join(path))?
+ .map_err(|_| eyre!("invalid address"))?;
+
+ Ok(url.to_string())
+}
+
pub async fn register(
address: &str,
username: &str,
@@ -46,14 +65,14 @@ pub async fn register(
map.insert("email", email);
map.insert("password", password);
- let url = format!("{address}/user/{username}");
+ let url = make_url(address, &format!("/user/{username}"))?;
let resp = reqwest::get(url).await?;
if resp.status().is_success() {
bail!("username already in use");
}
- let url = format!("{address}/register");
+ let url = make_url(address, "/register")?;
let client = reqwest::Client::new();
let resp = client
.post(url)
@@ -73,7 +92,7 @@ pub async fn register(
}
pub async fn login(address: &str, req: LoginRequest) -> Result<LoginResponse> {
- let url = format!("{address}/login");
+ let url = make_url(address, "/login")?;
let client = reqwest::Client::new();
let resp = client
@@ -197,7 +216,7 @@ impl<'a> Client<'a> {
}
pub async fn count(&self) -> Result<i64> {
- let url = format!("{}/sync/count", self.sync_addr);
+ let url = make_url(self.sync_addr, "/sync/count")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.get(url).send().await?;
@@ -217,7 +236,7 @@ impl<'a> Client<'a> {
}
pub async fn status(&self) -> Result<StatusResponse> {
- let url = format!("{}/sync/status", self.sync_addr);
+ let url = make_url(self.sync_addr, "/sync/status")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.get(url).send().await?;
@@ -233,7 +252,7 @@ impl<'a> Client<'a> {
}
pub async fn me(&self) -> Result<MeResponse> {
- let url = format!("{}/api/v0/me", self.sync_addr);
+ let url = make_url(self.sync_addr, "/api/v0/me")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.get(url).send().await?;
@@ -252,13 +271,15 @@ impl<'a> Client<'a> {
) -> Result<SyncHistoryResponse> {
let host = host.unwrap_or_else(|| hash_str(&get_host_user()));
- let url = format!(
- "{}/sync/history?sync_ts={}&history_ts={}&host={}",
+ let url = make_url(
self.sync_addr,
- urlencoding::encode(sync_ts.format(&Rfc3339)?.as_str()),
- urlencoding::encode(history_ts.format(&Rfc3339)?.as_str()),
- host,
- );
+ &format!(
+ "/sync/history?sync_ts={}&history_ts={}&host={}",
+ urlencoding::encode(sync_ts.format(&Rfc3339)?.as_str()),
+ urlencoding::encode(history_ts.format(&Rfc3339)?.as_str()),
+ host,
+ ),
+ )?;
let resp = self.client.get(url).send().await?;
let resp = handle_resp_error(resp).await?;
@@ -268,7 +289,7 @@ impl<'a> Client<'a> {
}
pub async fn post_history(&self, history: &[AddHistoryRequest]) -> Result<()> {
- let url = format!("{}/history", self.sync_addr);
+ let url = make_url(self.sync_addr, "/history")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.post(url).json(history).send().await?;
@@ -278,7 +299,7 @@ impl<'a> Client<'a> {
}
pub async fn delete_history(&self, h: History) -> Result<()> {
- let url = format!("{}/history", self.sync_addr);
+ let url = make_url(self.sync_addr, "/history")?;
let url = Url::parse(url.as_str())?;
let resp = self
@@ -296,7 +317,7 @@ impl<'a> Client<'a> {
}
pub async fn delete_store(&self) -> Result<()> {
- let url = format!("{}/api/v0/store", self.sync_addr);
+ let url = make_url(self.sync_addr, "/api/v0/store")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.delete(url).send().await?;
@@ -307,7 +328,7 @@ impl<'a> Client<'a> {
}
pub async fn post_records(&self, records: &[Record<EncryptedData>]) -> Result<()> {
- let url = format!("{}/api/v0/record", self.sync_addr);
+ let url = make_url(self.sync_addr, "/api/v0/record")?;
let url = Url::parse(url.as_str())?;
debug!("uploading {} records to {url}", records.len());
@@ -332,10 +353,13 @@ impl<'a> Client<'a> {
start
);
- let url = format!(
- "{}/api/v0/record/next?host={}&tag={}&count={}&start={}",
- self.sync_addr, host.0, tag, count, start
- );
+ let url = make_url(
+ self.sync_addr,
+ &format!(
+ "/api/v0/record/next?host={}&tag={}&count={}&start={}",
+ host.0, tag, count, start
+ ),
+ )?;
let url = Url::parse(url.as_str())?;
@@ -348,7 +372,7 @@ impl<'a> Client<'a> {
}
pub async fn record_status(&self) -> Result<RecordStatus> {
- let url = format!("{}/api/v0/record", self.sync_addr);
+ let url = make_url(self.sync_addr, "/api/v0/record")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.get(url).send().await?;
@@ -366,7 +390,7 @@ impl<'a> Client<'a> {
}
pub async fn delete(&self) -> Result<()> {
- let url = format!("{}/account", self.sync_addr);
+ let url = make_url(self.sync_addr, "/account")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.delete(url).send().await?;
@@ -385,7 +409,7 @@ impl<'a> Client<'a> {
current_password: String,
new_password: String,
) -> Result<()> {
- let url = format!("{}/account/password", self.sync_addr);
+ let url = make_url(self.sync_addr, "/account/password")?;
let url = Url::parse(url.as_str())?;
let resp = self
@@ -413,7 +437,7 @@ impl<'a> Client<'a> {
pub async fn verify(&self, token: Option<String>) -> Result<(bool, bool)> {
// could dedupe this a bit, but it's simple at the moment
let (email_sent, verified) = if let Some(token) = token {
- let url = format!("{}/api/v0/account/verify", self.sync_addr);
+ let url = make_url(self.sync_addr, "/api/v0/account/verify")?;
let url = Url::parse(url.as_str())?;
let resp = self
@@ -427,7 +451,7 @@ impl<'a> Client<'a> {
(false, resp.verified)
} else {
- let url = format!("{}/api/v0/account/send-verification", self.sync_addr);
+ let url = make_url(self.sync_addr, "/api/v0/account/send-verification")?;
let url = Url::parse(url.as_str())?;
let resp = self.client.post(url).send().await?;