Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/bitwarden-api-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ repository.workspace = true
license-file.workspace = true
keywords.workspace = true

[features]
uniffi = ["dep:bitwarden-uniffi-error", "dep:uniffi"]

[dependencies]
bitwarden-uniffi-error = { workspace = true, optional = true }
http = ">=1.4.0, <2"
reqwest = { workspace = true }
reqwest-middleware = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uniffi = { workspace = true, optional = true }
url = ">=2.5, <3"

[target.'cfg(not(target_arch="wasm32"))'.dependencies]
Expand Down
35 changes: 25 additions & 10 deletions crates/bitwarden-api-base/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
//! Error types for API operations.

use std::{error, fmt};
use std::{convert::Infallible, error, fmt, marker::PhantomData};

use serde::{Deserialize, Serialize};

/// Response content from a failed API call.
#[derive(Debug)]
pub struct ResponseContent<T = ()> {
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct ResponseContent {
/// HTTP status code of the response.
#[serde(with = "crate::status_code_serializer")]
pub status: reqwest::StatusCode,
/// Raw response body content.
pub content: String,
/// Deserialized entity from the response.
pub entity: Option<T>,
/// Response body content.
pub message: String,
}

/// Errors that can occur during API operations.
#[derive(Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
pub enum Error<T = ()> {
/// Error from the reqwest HTTP client.
Reqwest(reqwest::Error),
Expand All @@ -25,7 +28,12 @@ pub enum Error<T = ()> {
/// I/O error.
Io(std::io::Error),
/// API returned an error response.
ResponseError(ResponseContent<T>),
Response(ResponseContent),
Comment thread
dani-garcia marked this conversation as resolved.

/// Phantom variant to keep the unused `T` parameter alive without affecting downstream
/// `impl<T> From<Error<T>> for FooError` impls. Uninhabited via [`Infallible`].
#[doc(hidden)]
_Phantom(PhantomData<T>, Infallible),
}

impl<T> fmt::Display for Error<T> {
Expand All @@ -35,7 +43,8 @@ impl<T> fmt::Display for Error<T> {
Error::ReqwestMiddleware(e) => ("reqwest-middleware", e.to_string()),
Error::Serde(e) => ("serde", e.to_string()),
Error::Io(e) => ("IO", e.to_string()),
Error::ResponseError(e) => ("response", format!("status code {}", e.status)),
Error::Response(e) => ("response", format!("status code {}", e.status)),
Error::_Phantom(_, _) => unreachable!(),
};
write!(f, "error in {}: {}", module, e)
}
Expand All @@ -48,7 +57,7 @@ impl<T: fmt::Debug> error::Error for Error<T> {
Error::ReqwestMiddleware(e) => e,
Error::Serde(e) => e,
Error::Io(e) => e,
Error::ResponseError(_) => return None,
Error::Response(_) | Error::_Phantom(_, _) => return None,
})
}
}
Expand Down Expand Up @@ -76,3 +85,9 @@ impl<T> From<std::io::Error> for Error<T> {
Error::Io(e)
}
}

impl<T> From<ResponseContent> for Error<T> {
fn from(value: ResponseContent) -> Self {
Self::Response(value)
}
}
7 changes: 7 additions & 0 deletions crates/bitwarden-api-base/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#![doc = include_str!("../README.md")]

#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();

#[cfg(feature = "uniffi")]
mod uniffi_support;

mod client;
mod configuration;
mod error;
mod request;
mod status_code_serializer;
mod util;

pub use client::{new_http_client, new_http_client_builder};
Expand Down
16 changes: 8 additions & 8 deletions crates/bitwarden-api-base/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ async fn process_with_json_response_internal<E>(
)))),
}
} else {
Err(Error::ResponseError(ResponseContent {
Err(ResponseContent {
status,
content,
entity: None,
}))
message: content,
}
.into())
}
}

Expand All @@ -61,10 +61,10 @@ pub async fn process_with_empty_response<E>(
Ok(())
} else {
let content = response.text().await?;
Err(Error::ResponseError(ResponseContent {
Err(ResponseContent {
status,
content,
entity: None,
}))
message: content,
}
.into())
}
}
46 changes: 46 additions & 0 deletions crates/bitwarden-api-base/src/status_code_serializer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Serde helpers for [`http::StatusCode`], used via `#[serde(with = ...)]`.

use http::StatusCode;
use serde::{Deserialize, Deserializer, Serializer, de::Error};

pub fn serialize<S: Serializer>(status: &StatusCode, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_u16(status.as_u16())
}

pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<StatusCode, D::Error> {
let value = u16::deserialize(de)?;
StatusCode::from_u16(value).map_err(D::Error::custom)
}

#[cfg(test)]
mod tests {
use http::StatusCode;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Wrapper {
#[serde(with = "super")]
status: StatusCode,
}

#[test]
fn serializes_as_u16() {
let json = serde_json::to_string(&Wrapper {
status: StatusCode::NOT_FOUND,
})
.unwrap();
assert_eq!(json, r#"{"status":404}"#);
}

#[test]
fn deserializes_from_u16() {
let wrapper: Wrapper = serde_json::from_str(r#"{"status":201}"#).unwrap();
assert_eq!(wrapper.status, StatusCode::CREATED);
}

#[test]
fn rejects_invalid_status() {
let err = serde_json::from_str::<Wrapper>(r#"{"status":99}"#).unwrap_err();
assert!(err.to_string().contains("invalid status code"));
}
}
10 changes: 10 additions & 0 deletions crates/bitwarden-api-base/src/uniffi_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Custom Uniffi type converters for types defined by external crates.

use bitwarden_uniffi_error::convert_result;
use reqwest::StatusCode;

uniffi::custom_type!(StatusCode, u16, {
remote,
try_lift: |val| convert_result(StatusCode::from_u16(val)),
lower: |obj| obj.as_u16(),
});
9 changes: 9 additions & 0 deletions crates/bitwarden-api-base/uniffi.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[bindings.kotlin]
package_name = "com.bitwarden.api.base"
generate_immutable_records = true
android = true

[bindings.swift]
ffi_module_name = "BitwardenApiBaseFFI"
module_name = "BitwardenApiBase"
generate_immutable_records = true
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl LoginClient {
mod tests {
use std::num::NonZeroU32;

use bitwarden_api_api::ResponseContent;
use bitwarden_api_identity::models::KdfType;
use bitwarden_core::{ClientSettings, DeviceType};
use bitwarden_crypto::Kdf;
Expand Down Expand Up @@ -262,13 +263,13 @@ mod tests {

assert!(result.is_err());
match result.unwrap_err() {
PasswordPreloginError::Api(bitwarden_core::ApiError::ResponseContent {
PasswordPreloginError::Api(bitwarden_core::ApiError::Response(ResponseContent {
status,
message: _,
}) => {
})) => {
assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR);
}
other => panic!("Expected Api ResponseContent error, got {:?}", other),
other => panic!("Expected Api Response error, got {:?}", other),
}
}
}
1 change: 1 addition & 0 deletions crates/bitwarden-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ no-memory-hardening = [
secrets = [] # Secrets manager API
uniffi = [
"internal",
"bitwarden-api-base/uniffi",
"bitwarden-crypto/uniffi",
"bitwarden-encoding/uniffi",
"dep:bitwarden-uniffi-error",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
use bitwarden_api_base::ResponseContent;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};

use crate::{
ApiError,
auth::{
api::response::{
IdentityTokenFailResponse, IdentityTokenPayloadResponse, IdentityTokenRefreshResponse,
IdentityTokenSuccessResponse, IdentityTwoFactorResponse,
},
login::LoginError,
use crate::auth::{
api::response::{
IdentityTokenFailResponse, IdentityTokenPayloadResponse, IdentityTokenRefreshResponse,
IdentityTokenSuccessResponse, IdentityTwoFactorResponse,
},
login::LoginError,
};

#[derive(Debug, Serialize, Deserialize, PartialEq)]
Expand All @@ -35,11 +33,13 @@ pub(crate) fn parse_identity_response(
} else if let Ok(r) = serde_json::from_str::<IdentityTokenFailResponse>(&response) {
Err(LoginError::IdentityFail(r))
} else {
Err(ApiError::ResponseContent {
status,
message: response,
}
.into())
Err(LoginError::Api(
ResponseContent {
status,
message: response,
}
.into(),
))
}
}

Expand Down
19 changes: 11 additions & 8 deletions crates/bitwarden-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

use std::fmt::Debug;

use bitwarden_api_base::Error as BaseApiError;
use bitwarden_api_base::{Error as BaseApiError, ResponseContent};
#[cfg(feature = "internal")]
use bitwarden_error::bitwarden_error;
use reqwest::StatusCode;
use thiserror::Error;

/// Errors from performing network requests.
Expand All @@ -22,25 +21,29 @@ pub enum ApiError {
#[error(transparent)]
Io(#[from] std::io::Error),

#[error("Received error message from server: [{}] {}", .status, .message)]
ResponseContent { status: StatusCode, message: String },
#[error("Received error message from server: [{}] {}", _0.status, _0.message)]
Response(ResponseContent),
}

impl<T> From<BaseApiError<T>> for ApiError {
fn from(e: BaseApiError<T>) -> Self {
match e {
BaseApiError::Reqwest(e) => Self::Reqwest(e),
BaseApiError::ReqwestMiddleware(e) => Self::ReqwestMiddleware(e),
BaseApiError::ResponseError(e) => Self::ResponseContent {
status: e.status,
message: e.content,
},
BaseApiError::Response(e) => Self::Response(e),
BaseApiError::Serde(e) => Self::Serde(e),
BaseApiError::Io(e) => Self::Io(e),
BaseApiError::_Phantom(_, _) => unreachable!(),
}
}
}

impl From<ResponseContent> for ApiError {
fn from(value: ResponseContent) -> Self {
Self::Response(value)
}
}

/// Client is not authenticated or the session has expired.
#[derive(Debug, Error)]
#[error("The client is not authenticated or the session has expired")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async fn check_key_pair(
// Step 3: Fetch key pair from server. A 404 means the user has no keys at all.
let keys_response = match api_client.accounts_api().get_keys().await {
Ok(response) => response,
Err(bitwarden_api_api::apis::Error::ResponseError(e))
Err(bitwarden_api_api::apis::Error::Response(e))
if e.status == reqwest::StatusCode::NOT_FOUND =>
{
info!("User has no public key encryption key pair (404), regeneration needed");
Expand Down Expand Up @@ -348,11 +348,10 @@ mod tests {

let api_client = ApiClient::new_mocked(|mock| {
mock.accounts_api.expect_get_keys().once().returning(|| {
Err(bitwarden_api_api::apis::Error::ResponseError(
Err(bitwarden_api_api::apis::Error::Response(
bitwarden_api_api::apis::ResponseContent {
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
content: "Internal Server Error".to_string(),
entity: None,
message: "Internal Server Error".to_string(),
},
))
});
Expand All @@ -374,11 +373,10 @@ mod tests {

let api_client = ApiClient::new_mocked(|mock| {
mock.accounts_api.expect_get_keys().once().returning(|| {
Err(bitwarden_api_api::apis::Error::ResponseError(
Err(bitwarden_api_api::apis::Error::Response(
bitwarden_api_api::apis::ResponseContent {
status: reqwest::StatusCode::NOT_FOUND,
content: "Not Found".to_string(),
entity: None,
message: "Not Found".to_string(),
},
))
});
Expand Down
Loading