Skip to content

Commit a7d36f5

Browse files
authored
feat: Add missing PUT key to the Rust client, replace anyhow with… (#191)
… `thiserror` We did a 360 when figuring out whether we should assign objects keys from the server or the client. In that process, I guess I forgot to add back the `key` to the Rust client. So here it is. Also, this replaces our usage of `anyhow` within the `types` and `client` crate with `thiserror`. This is a bit verbose, and I can predict for a fact that noone will ever match on these errors or care about which one it is. But relay folks insist. They seem to have a "no `anyhow`" policy, as they claim capturing backtraces on every `anyhow` that happens is expensive. While that is true, they have also pinned a >2 year old `anyhow` version which supposedly does not capture backtraces by default. And relay team also does not seem aware that you can actually disable backtraces for errors by setting `RUST_LIB_BACKTRACE=0`. But hey, I don’t really want to argue. If they want to cargo-cult around these decisions without challenging them every now and then, who am I to tell them otherwise?
1 parent a789512 commit a7d36f5

File tree

12 files changed

+101
-29
lines changed

12 files changed

+101
-29
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ sentry = { version = "0.45.0" }
4343
serde = { version = "1.0.219", features = ["derive"] }
4444
serde_json = "1.0.140"
4545
tempfile = "3.20.0"
46+
thiserror = "2.0.17"
4647
tokio = "1.47.0"
4748
tokio-stream = "0.1.17"
4849
tokio-util = "0.7.15"

clients/rust/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ rust-version = "1.89"
1111
publish = true
1212

1313
[dependencies]
14-
anyhow = { workspace = true }
1514
async-compression = { version = "0.4.27", features = ["tokio", "zstd"] }
1615
bytes = { workspace = true }
1716
futures-util = { workspace = true }
1817
objectstore-types = { workspace = true }
1918
reqwest = { workspace = true, features = ["json", "stream"] }
19+
sentry = { workspace = true }
2020
serde = { workspace = true }
21+
thiserror = { workspace = true }
2122
tokio = { workspace = true }
2223
tokio-util = { workspace = true, features = ["io"] }
23-
sentry = { workspace = true }
2424

2525
[dev-dependencies]
2626
objectstore-server = { workspace = true }

clients/rust/src/client.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use std::time::Duration;
55
use bytes::Bytes;
66
use futures_util::stream::BoxStream;
77
use objectstore_types::ExpirationPolicy;
8-
use reqwest::header::HeaderName;
98

109
pub use objectstore_types::{Compression, PARAM_SCOPE, PARAM_USECASE};
1110

@@ -36,7 +35,7 @@ impl ClientBuilder {
3635
///
3736
/// In order to get or put objects, one has to create a [`Client`] using the
3837
/// [`for_organization`](Self::for_organization) function.
39-
pub fn new(service_url: &str, usecase: &str) -> anyhow::Result<Self> {
38+
pub fn new(service_url: &str, usecase: &str) -> crate::Result<Self> {
4039
let client = reqwest::Client::builder()
4140
.user_agent(USER_AGENT)
4241
// hickory-dns: Controlled by the `reqwest/hickory-dns` feature flag
@@ -142,7 +141,7 @@ impl Client {
142141
&self,
143142
method: reqwest::Method,
144143
uri: U,
145-
) -> anyhow::Result<reqwest::RequestBuilder> {
144+
) -> crate::Result<reqwest::RequestBuilder> {
146145
let mut builder = self.http.request(method, uri).query(&[
147146
(PARAM_SCOPE, self.scope.as_ref()),
148147
(PARAM_USECASE, self.usecase.as_ref()),
@@ -152,15 +151,15 @@ impl Client {
152151
let trace_headers =
153152
sentry::configure_scope(|scope| Some(scope.iter_trace_propagation_headers()));
154153
for (header_name, value) in trace_headers.into_iter().flatten() {
155-
builder = builder.header(HeaderName::try_from(header_name)?, value);
154+
builder = builder.header(header_name, value);
156155
}
157156
}
158157

159158
Ok(builder)
160159
}
161160

162161
/// Deletes the object with the given `id`.
163-
pub async fn delete(&self, id: &str) -> anyhow::Result<()> {
162+
pub async fn delete(&self, id: &str) -> crate::Result<()> {
164163
let delete_url = format!("{}/v1/{id}", self.service_url);
165164

166165
let _response = self

clients/rust/src/error.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// Errors that can happen within the objectstore-client
2+
#[derive(Debug, thiserror::Error)]
3+
pub enum Error {
4+
/// Any error emitted from the underlying [`reqwest`] client.
5+
#[error(transparent)]
6+
Reqwest(#[from] reqwest::Error),
7+
/// IO errors related to payload streaming.
8+
#[error(transparent)]
9+
Io(#[from] std::io::Error),
10+
/// Errors related to UTF-8 dcoding
11+
#[error(transparent)]
12+
Utf8(#[from] std::string::FromUtf8Error),
13+
/// Errors handling metadata, such as serializing it to/from HTTP headers.
14+
#[error(transparent)]
15+
Metadata(#[from] objectstore_types::Error),
16+
}
17+
18+
/// A convenience alias that defaults our [`Error`] type.
19+
pub type Result<T, E = Error> = std::result::Result<T, E>;

clients/rust/src/get.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ pub struct GetResult {
2323

2424
impl GetResult {
2525
/// Loads the object payload fully into memory.
26-
pub async fn payload(self) -> anyhow::Result<bytes::Bytes> {
26+
pub async fn payload(self) -> crate::Result<bytes::Bytes> {
2727
let bytes: BytesMut = self.stream.try_collect().await?;
2828
Ok(bytes.freeze())
2929
}
3030

3131
/// Loads the object payload fully into memory and interprets it as UTF-8 text.
32-
pub async fn text(self) -> anyhow::Result<String> {
32+
pub async fn text(self) -> crate::Result<String> {
3333
let bytes = self.payload().await?;
3434
Ok(String::from_utf8(bytes.to_vec())?)
3535
}
@@ -73,7 +73,7 @@ impl GetBuilder<'_> {
7373
}
7474

7575
/// Sends the `GET` request.
76-
pub async fn send(self) -> anyhow::Result<Option<GetResult>> {
76+
pub async fn send(self) -> crate::Result<Option<GetResult>> {
7777
let get_url = format!("{}/v1/{}", self.client.service_url, self.id);
7878

7979
let response = self

clients/rust/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//! use objectstore_client::ClientBuilder;
1111
//!
1212
//! #[tokio::main]
13-
//! # async fn main() -> anyhow::Result<()> {
13+
//! # async fn main() -> objectstore_client::Result<()> {
1414
//! let client = ClientBuilder::new("http://localhost:8888/", "my-usecase")?
1515
//! .for_organization(42);
1616
//!
@@ -24,12 +24,14 @@
2424
#![warn(missing_debug_implementations)]
2525

2626
mod client;
27+
mod error;
2728
mod get;
2829
mod put;
2930

3031
pub use objectstore_types::{Compression, ExpirationPolicy};
3132

3233
pub use client::*;
34+
pub use error::*;
3335
pub use get::*;
3436
pub use put::*;
3537

clients/rust/src/put.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ impl Client {
2626
PutBuilder {
2727
client: self,
2828
metadata,
29+
key: None,
2930
body,
3031
}
3132
}
@@ -55,6 +56,7 @@ impl Client {
5556
pub struct PutBuilder<'a> {
5657
pub(crate) client: &'a Client,
5758
pub(crate) metadata: Metadata,
59+
pub(crate) key: Option<String>,
5860
pub(crate) body: PutBody,
5961
}
6062

@@ -69,6 +71,15 @@ impl fmt::Debug for PutBody {
6971
}
7072

7173
impl PutBuilder<'_> {
74+
/// Sets an explicit object key.
75+
///
76+
/// If a key is specified, the object will be stored under that key. Otherwise, the objectstore
77+
/// server will automatically assign a random key, which is then returned from this request.
78+
pub fn key(mut self, key: impl Into<String>) -> Self {
79+
self.key = Some(key.into());
80+
self
81+
}
82+
7283
/// Sets an explicit compression algorithm to be used for this payload.
7384
///
7485
/// [`None`] should be used if no compression should be performed by the client,
@@ -112,8 +123,12 @@ pub struct PutResponse {
112123
// and "impl trait in associated type position" is not yet stable :-(
113124
impl PutBuilder<'_> {
114125
/// Sends the built PUT request to the upstream service.
115-
pub async fn send(self) -> anyhow::Result<PutResponse> {
116-
let put_url = format!("{}/v1/", self.client.service_url);
126+
pub async fn send(self) -> crate::Result<PutResponse> {
127+
let put_url = format!(
128+
"{}/v1/{}",
129+
self.client.service_url,
130+
self.key.as_deref().unwrap_or_default()
131+
);
117132
let mut builder = self.client.request(reqwest::Method::PUT, put_url)?;
118133

119134
let body = match (self.metadata.compression, self.body) {

objectstore-server/src/endpoints.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::io;
44

5+
use anyhow::Context;
56
use axum::body::Body;
67
use axum::extract::{Path, Query, State};
78
use axum::http::{HeaderMap, StatusCode};
@@ -54,7 +55,8 @@ async fn put_object_nokey(
5455
key: uuid::Uuid::new_v4().to_string(),
5556
};
5657
populate_sentry_scope(&path);
57-
let metadata = Metadata::from_headers(&headers, "")?;
58+
let metadata =
59+
Metadata::from_headers(&headers, "").context("extracting metadata from headers")?;
5860

5961
let stream = body.into_data_stream().map_err(io::Error::other).boxed();
6062
let key = state.service.put_object(path, &metadata, stream).await?;
@@ -77,7 +79,8 @@ async fn put_object(
7779
key,
7880
};
7981
populate_sentry_scope(&path);
80-
let metadata = Metadata::from_headers(&headers, "")?;
82+
let metadata =
83+
Metadata::from_headers(&headers, "").context("extracting metadata from headers")?;
8184

8285
let stream = body.into_data_stream().map_err(io::Error::other).boxed();
8386
let key = state.service.put_object(path, &metadata, stream).await?;
@@ -103,7 +106,9 @@ async fn get_object(
103106
return Ok(StatusCode::NOT_FOUND.into_response());
104107
};
105108

106-
let headers = metadata.to_headers("", false)?;
109+
let headers = metadata
110+
.to_headers("", false)
111+
.context("extracting metadata from headers")?;
107112
Ok((headers, Body::from_stream(stream)).into_response())
108113
}
109114

objectstore-types/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ edition = "2024"
1010
publish = true
1111

1212
[dependencies]
13-
anyhow = { workspace = true }
14-
humantime = { workspace = true }
1513
http = { workspace = true }
14+
humantime = { workspace = true }
1615
serde = { workspace = true }
16+
thiserror = { workspace = true }

0 commit comments

Comments
 (0)