-
Notifications
You must be signed in to change notification settings - Fork 3
refactor: replace unwrap with error handling in Client methods #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ use crate::error::AppwriteError; | |||||
| use crate::error::Result; | ||||||
| use crate::input_file::InputFile; | ||||||
| use arc_swap::ArcSwap; | ||||||
| use reqwest::{header::HeaderMap, multipart, Client as HttpClient, Method, Response}; | ||||||
| use reqwest::{header::{HeaderMap, HeaderValue}, multipart, Client as HttpClient, Method, Response}; | ||||||
| use serde::de::DeserializeOwned; | ||||||
| use serde_json::Value; | ||||||
| use std::collections::HashMap; | ||||||
|
|
@@ -88,12 +88,18 @@ impl Client { | |||||
| /// Create a new Appwrite client | ||||||
| pub fn new() -> Self { | ||||||
| let mut headers = HeaderMap::new(); | ||||||
| headers.insert("X-Appwrite-Response-Format", "1.9.0".parse().unwrap()); | ||||||
| headers.insert("user-agent", format!("AppwriteRustSDK/0.1.0 ({}; {})", std::env::consts::OS, std::env::consts::ARCH).parse().unwrap()); | ||||||
| headers.insert("x-sdk-name", "Rust".parse().unwrap()); | ||||||
| headers.insert("x-sdk-platform", "server".parse().unwrap()); | ||||||
| headers.insert("x-sdk-language", "rust".parse().unwrap()); | ||||||
| headers.insert("x-sdk-version", "0.1.0".parse().unwrap()); | ||||||
| headers.insert("X-Appwrite-Response-Format", HeaderValue::from_static("1.9.0")); | ||||||
| // user-agent contains runtime OS/arch info, so it must be built dynamically | ||||||
| headers.insert( | ||||||
| "user-agent", | ||||||
| format!("AppwriteRustSDK/0.1.0 ({}; {})", std::env::consts::OS, std::env::consts::ARCH) | ||||||
| .parse() | ||||||
| .expect("OS and ARCH constants produce valid ASCII"), | ||||||
| ); | ||||||
| headers.insert("x-sdk-name", HeaderValue::from_static("Rust")); | ||||||
| headers.insert("x-sdk-platform", HeaderValue::from_static("server")); | ||||||
| headers.insert("x-sdk-language", HeaderValue::from_static("rust")); | ||||||
| headers.insert("x-sdk-version", HeaderValue::from_static("0.1.0")); | ||||||
|
|
||||||
| let config = Config { | ||||||
| endpoint: "https://cloud.appwrite.io/v1".to_string(), | ||||||
|
|
@@ -103,8 +109,9 @@ impl Client { | |||||
| timeout_secs: DEFAULT_TIMEOUT, | ||||||
| }; | ||||||
|
|
||||||
| let http = Self::build_http_client(&config); | ||||||
| let http_no_redirect = Self::build_http_client_no_redirect(&config); | ||||||
| // SAFETY: Default config uses known-valid values, so build cannot fail | ||||||
| let http = Self::build_http_client(&config).unwrap(); | ||||||
| let http_no_redirect = Self::build_http_client_no_redirect(&config).unwrap(); | ||||||
|
|
||||||
| let state = ClientState { config, http, http_no_redirect }; | ||||||
|
|
||||||
|
|
@@ -113,17 +120,19 @@ impl Client { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| fn build_http_client(config: &Config) -> HttpClient { | ||||||
| fn build_http_client(config: &Config) -> Result<HttpClient> { | ||||||
| let mut builder = HttpClient::builder().timeout(Duration::from_secs(config.timeout_secs)); | ||||||
|
|
||||||
| if config.self_signed { | ||||||
| builder = builder.danger_accept_invalid_certs(true); | ||||||
| } | ||||||
|
|
||||||
| builder.build().expect("Failed to create HTTP client") | ||||||
| builder.build().map_err(|e| { | ||||||
| AppwriteError::new(0, format!("Failed to create HTTP client: {}", e), None, String::new()) | ||||||
| }) | ||||||
| } | ||||||
|
|
||||||
| fn build_http_client_no_redirect(config: &Config) -> HttpClient { | ||||||
| fn build_http_client_no_redirect(config: &Config) -> Result<HttpClient> { | ||||||
| let mut builder = HttpClient::builder() | ||||||
| .redirect(reqwest::redirect::Policy::none()) | ||||||
| .timeout(Duration::from_secs(config.timeout_secs)); | ||||||
|
|
@@ -132,90 +141,117 @@ impl Client { | |||||
| builder = builder.danger_accept_invalid_certs(true); | ||||||
| } | ||||||
|
|
||||||
| builder.build().expect("Failed to create no-redirect HTTP client") | ||||||
| builder.build().map_err(|e| { | ||||||
| AppwriteError::new(0, format!("Failed to create no-redirect HTTP client: {}", e), None, String::new()) | ||||||
| }) | ||||||
| } | ||||||
|
|
||||||
| /// Set the API endpoint | ||||||
| pub fn set_endpoint<S: Into<String>>(&self, endpoint: S) -> Self { | ||||||
| pub fn set_endpoint<S: Into<String>>(&self, endpoint: S) -> Result<Self> { | ||||||
| let endpoint = endpoint.into(); | ||||||
| if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") { | ||||||
| panic!("Invalid endpoint URL: {}. Endpoint must start with http:// or https://", endpoint); | ||||||
| return Err(AppwriteError::new( | ||||||
| 0, | ||||||
| format!("Invalid endpoint URL: {}. Endpoint must start with http:// or https://", endpoint), | ||||||
| None, | ||||||
| String::new(), | ||||||
| )); | ||||||
| } | ||||||
| self.state.rcu(|state| { | ||||||
| let mut next = (**state).clone(); | ||||||
| next.config.endpoint = endpoint.clone(); | ||||||
| Arc::new(next) | ||||||
| }); | ||||||
| self.clone() | ||||||
| Ok(self.clone()) | ||||||
| } | ||||||
|
|
||||||
| /// Set the project ID | ||||||
| pub fn set_project<S: Into<String>>(&self, project: S) -> Self { | ||||||
| pub fn set_project<S: Into<String>>(&self, project: S) -> Result<Self> { | ||||||
| let project = project.into(); | ||||||
| let header_value: HeaderValue = project.parse().map_err(|_| { | ||||||
| AppwriteError::new(0, format!("Invalid header value for project: {}", project), None, String::new()) | ||||||
| })?; | ||||||
| self.state.rcu(|state| { | ||||||
| let mut next = (**state).clone(); | ||||||
| next.config.headers.insert("x-appwrite-project", project.clone().parse().unwrap()); | ||||||
| next.config.headers.insert("x-appwrite-project", header_value.clone()); | ||||||
| Arc::new(next) | ||||||
| }); | ||||||
| self.clone() | ||||||
| Ok(self.clone()) | ||||||
| } | ||||||
|
Comment on lines
163
to
180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
All validation Consider using return Err(AppwriteError::new(
400,
format!("Invalid endpoint URL: {}. Endpoint must start with http:// or https://", endpoint),
None,
String::new(),
));The same applies to all other validation errors in |
||||||
|
|
||||||
| /// Set the API key | ||||||
| pub fn set_key<S: Into<String>>(&self, key: S) -> Self { | ||||||
| pub fn set_key<S: Into<String>>(&self, key: S) -> Result<Self> { | ||||||
| let key = key.into(); | ||||||
| let header_value: HeaderValue = key.parse().map_err(|_| { | ||||||
| AppwriteError::new(0, format!("Invalid header value for key: {}", key), None, String::new()) | ||||||
|
||||||
| AppwriteError::new(0, format!("Invalid header value for key: {}", key), None, String::new()) | |
| AppwriteError::new(0, format!("Invalid header value for key (length {} characters)", key.len()), None, String::new()) |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message for an invalid JWT includes the full jwt value. JWTs are credentials and should not be echoed back in errors because they can end up in logs/telemetry. Use a redacted message that does not include the token contents.
| AppwriteError::new(0, format!("Invalid header value for JWT: {}", jwt), None, String::new()) | |
| AppwriteError::new(0, "Invalid header value for JWT".to_string(), None, String::new()) |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message for an invalid session includes the full session value, which is typically sensitive. Avoid including session tokens in error messages; return a redacted message instead.
| AppwriteError::new(0, format!("Invalid header value for session: {}", session), None, String::new()) | |
| AppwriteError::new(0, "Invalid header value for session".to_string(), None, String::new()) |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_self_signed now returns Result<Self>, but it never returns Err: it updates next.config.self_signed even if rebuilding the HTTP clients fails, silently keeping the old clients. This can leave config and the actual http/http_no_redirect clients out of sync. Consider attempting the rebuild first and returning Err without mutating state if the rebuild fails (or keep the Self return type if failures are intentionally ignored).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failure leaves config and HTTP client in inconsistent state
In set_self_signed (and the same pattern in set_timeout), when build_http_client fails the code updates next.config.self_signed but keeps the old HTTP client. This means the client's config says self_signed: true while its internal reqwest::Client still rejects self-signed certificates — a silently broken state.
Worse, the method always returns Ok(self.clone()) regardless of whether the build succeeded. The Result<Self> return type is therefore misleading: callers have no way to detect the failure.
A safer approach is to build both clients outside the rcu closure first, propagate any error before touching state, and only commit the new state when both builds succeed:
pub fn set_self_signed(&self, self_signed: bool) -> Result<Self> {
let current_config = {
let state = self.state.load();
state.config.clone()
};
if current_config.self_signed == self_signed {
return Ok(self.clone());
}
let mut new_config = current_config;
new_config.self_signed = self_signed;
let new_http = Self::build_http_client(&new_config)?;
let new_http_no_redirect = Self::build_http_client_no_redirect(&new_config)?;
self.state.rcu(|state| {
let mut next = (**state).clone();
next.config.self_signed = self_signed;
next.http = new_http.clone();
next.http_no_redirect = new_http_no_redirect.clone();
Arc::new(next)
});
Ok(self.clone())
}Apply the same pattern to set_timeout.
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_timeout returns Result<Self> but never returns Err, and it updates next.config.timeout_secs even if rebuilding the HTTP clients fails, keeping the old clients. That can desynchronize the stored config from the actual client behavior (timeouts may not change). Consider rebuilding first and returning Err (and not updating state) when the rebuild fails, or otherwise avoid a Result return type if failure is intentionally ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Client::new()still calls.unwrap()onbuild_http_client*()results.reqwest::ClientBuilder::build()can fail (e.g., TLS backend/init, system proxy/env issues), so this can still panic at runtime, and the// SAFETY: ... build cannot failcomment is not guaranteed. Consider returningResult<Client>(or adding atry_new()that returnsResult) and propagating the error instead of unwrapping.