Skip to content

Commit 9e72329

Browse files
committed
feat(error): differentiate zome call error responses
1 parent 3d78b96 commit 9e72329

File tree

6 files changed

+214
-64
lines changed

6 files changed

+214
-64
lines changed

src/error.rs

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
//! hc-http-gw error types
22
3+
use crate::app_selection::AppSelectionError;
34
use axum::http::StatusCode;
45
use axum::response::IntoResponse;
56
use axum::Json;
67
use serde::{Deserialize, Serialize};
78

8-
use crate::app_selection::AppSelectionError;
9-
109
/// Core HTTP Gateway error type
1110
#[derive(thiserror::Error, Debug)]
1211
pub enum HcHttpGatewayError {
@@ -36,7 +35,7 @@ pub enum HcHttpGatewayError {
3635
AppSelectionError(#[from] AppSelectionError),
3736
}
3837

39-
/// Type aliased Result
38+
/// Gateway result type.
4039
pub type HcHttpGatewayResult<T> = Result<T, HcHttpGatewayError>;
4140

4241
/// Error format returned to the caller.
@@ -52,52 +51,38 @@ impl From<String> for ErrorResponse {
5251
}
5352
}
5453

55-
impl From<&str> for ErrorResponse {
56-
fn from(value: &str) -> Self {
57-
Self {
58-
error: value.to_owned(),
59-
}
60-
}
61-
}
62-
63-
impl IntoResponse for HcHttpGatewayError {
64-
fn into_response(self) -> axum::response::Response {
54+
impl HcHttpGatewayError {
55+
/// Convert error into HTTP status code and error message.
56+
pub fn into_status_code_and_body(self) -> (StatusCode, String) {
6557
match self {
66-
HcHttpGatewayError::RequestMalformed(e) => (
67-
StatusCode::BAD_REQUEST,
68-
Json(ErrorResponse::from(format!("Request is malformed: {e}"))),
69-
),
70-
HcHttpGatewayError::UnauthorizedFunction {
71-
app_id,
72-
zome_name,
73-
fn_name,
74-
} => (
75-
StatusCode::FORBIDDEN,
76-
Json(ErrorResponse::from(format!(
77-
"Function {fn_name} in zome {zome_name} in app {app_id} is not allowed"
78-
))),
79-
),
58+
HcHttpGatewayError::RequestMalformed(_) => (StatusCode::BAD_REQUEST, self.to_string()),
59+
HcHttpGatewayError::UnauthorizedFunction { .. } => {
60+
(StatusCode::FORBIDDEN, self.to_string())
61+
}
8062
HcHttpGatewayError::UpstreamUnavailable => (
8163
StatusCode::BAD_GATEWAY,
82-
Json(ErrorResponse::from("Could not connect to Holochain")),
83-
),
84-
HcHttpGatewayError::AppSelectionError(AppSelectionError::NotInstalled) => (
85-
StatusCode::NOT_FOUND,
86-
Json(ErrorResponse::from(self.to_string())),
87-
),
88-
HcHttpGatewayError::AppSelectionError(AppSelectionError::NotAllowed) => (
89-
StatusCode::FORBIDDEN,
90-
Json(ErrorResponse::from(self.to_string())),
91-
),
92-
HcHttpGatewayError::AppSelectionError(AppSelectionError::MultipleMatching) => (
93-
StatusCode::INTERNAL_SERVER_ERROR,
94-
Json(ErrorResponse::from(self.to_string())),
64+
"Could not connect to Holochain".to_string(),
9565
),
66+
HcHttpGatewayError::AppSelectionError(AppSelectionError::NotInstalled) => {
67+
(StatusCode::NOT_FOUND, self.to_string())
68+
}
69+
HcHttpGatewayError::AppSelectionError(AppSelectionError::NotAllowed) => {
70+
(StatusCode::FORBIDDEN, self.to_string())
71+
}
72+
HcHttpGatewayError::AppSelectionError(AppSelectionError::MultipleMatching) => {
73+
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
74+
}
9675
_ => (
9776
StatusCode::INTERNAL_SERVER_ERROR,
98-
Json(ErrorResponse::from("Something went wrong")),
77+
"Something went wrong".to_string(),
9978
),
10079
}
101-
.into_response()
80+
}
81+
}
82+
83+
impl IntoResponse for HcHttpGatewayError {
84+
fn into_response(self) -> axum::response::Response {
85+
let (status_code, body) = self.into_status_code_and_body();
86+
(status_code, Json(ErrorResponse::from(body))).into_response()
10287
}
10388
}

src/holochain/app_conn_pool.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -346,23 +346,32 @@ impl AppCall for AppConnPool {
346346
payload: ExternIO,
347347
) -> BoxFuture<'static, HcHttpGatewayResult<ExternIO>> {
348348
let this = self.clone();
349+
let app_id = installed_app_id.clone();
349350
Box::pin(async move {
350351
this.call(installed_app_id, |app_ws| {
352+
let app_id = app_id.clone();
351353
let cell_id = cell_id.clone();
352354
let zome_name = zome_name.clone();
353355
let fn_name = fn_name.clone();
354356
let payload = payload.clone();
355357
Box::pin(async move {
356358
let result = app_ws
357359
.call_zome(
358-
ZomeCallTarget::CellId(cell_id),
359-
zome_name.into(),
360-
fn_name.into(),
360+
ZomeCallTarget::CellId(cell_id.clone()),
361+
zome_name.clone().into(),
362+
fn_name.clone().into(),
361363
payload,
362364
)
363365
.await;
364366
if let Err(err) = &result {
365-
tracing::debug!(?err);
367+
tracing::debug!(
368+
?err,
369+
?app_id,
370+
?cell_id,
371+
?zome_name,
372+
?fn_name,
373+
"Zome call error"
374+
);
366375
}
367376
let result = result?;
368377
Ok(result)

src/routes/zome_call/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
// DnaHash::from_raw_32(vec![1; 32]).to_string()
2+
const DNA_HASH: &str = "uhC0kAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQF-z86-";
3+
14
mod responses;
25
mod validations;
Lines changed: 163 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,202 @@
1+
use super::DNA_HASH;
12
use crate::config::{AllowedFns, Configuration};
23
use crate::test::data::new_test_app_info;
34
use crate::test::router::TestRouter;
45
use crate::{MockAdminCall, MockAppCall};
5-
use holochain_client::ExternIO;
6+
use holochain_client::{ConductorApiError, ExternIO};
7+
use holochain_conductor_api::ExternalApiWireError;
68
use holochain_types::prelude::DnaHash;
79
use reqwest::StatusCode;
810
use std::collections::HashMap;
911
use std::net::{Ipv4Addr, SocketAddr};
1012
use std::sync::Arc;
1113

12-
#[tokio::test(flavor = "multi_thread")]
13-
async fn happy_zome_call() {
14-
let app_id = "tapp";
15-
let dna_hash = DnaHash::from_raw_32(vec![1; 32]);
14+
const APP_ID: &str = "tapp";
1615

16+
fn create_test_router(app_call: MockAppCall) -> TestRouter {
1717
let mut allowed_fns = HashMap::new();
18-
allowed_fns.insert(app_id.into(), AllowedFns::All);
18+
allowed_fns.insert(APP_ID.into(), AllowedFns::All);
1919
let config = Configuration::try_new(
2020
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8888),
2121
"1024",
22-
app_id,
22+
APP_ID,
2323
allowed_fns,
2424
"",
2525
"",
2626
)
2727
.unwrap();
2828

2929
let mut admin_call = MockAdminCall::new();
30-
let dna_hash2 = dna_hash.clone();
3130
admin_call.expect_list_apps().returning(move |_| {
32-
let dna_hash = dna_hash2.clone();
3331
Box::pin(async move {
34-
let app_info = new_test_app_info(app_id, dna_hash);
32+
let app_info = new_test_app_info(APP_ID, DnaHash::from_raw_32(vec![1; 32]));
3533
Ok(vec![app_info])
3634
})
3735
});
3836
let admin_call = Arc::new(admin_call);
37+
let app_call = Arc::new(app_call);
38+
TestRouter::new_with_config_and_interfaces(config, admin_call, app_call)
39+
}
40+
41+
#[tokio::test]
42+
async fn happy_zome_call() {
3943
let mut app_call = MockAppCall::new();
4044
app_call
4145
.expect_handle_zome_call()
4246
.returning(|_, _, _, _, _| {
4347
Box::pin(async move { Ok(ExternIO::encode("return_value").unwrap()) })
4448
});
45-
let app_call = Arc::new(app_call);
46-
let router = TestRouter::new_with_config_and_interfaces(config, admin_call, app_call);
49+
let router = create_test_router(app_call);
4750
let (status_code, body) = router
48-
.request(&format!("/{dna_hash}/{app_id}/coordinator/fn_name"))
51+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
4952
.await;
5053
assert_eq!(status_code, StatusCode::OK);
5154
assert_eq!(body, r#""return_value""#);
5255
}
56+
57+
#[tokio::test]
58+
async fn app_not_found() {
59+
let mut app_call = MockAppCall::new();
60+
app_call
61+
.expect_handle_zome_call()
62+
.returning(|_, _, _, _, _| {
63+
Box::pin(async move {
64+
Err(crate::HcHttpGatewayError::HolochainError(
65+
ConductorApiError::AppNotFound,
66+
))
67+
})
68+
});
69+
let router = create_test_router(app_call);
70+
let (status_code, body) = router
71+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
72+
.await;
73+
// The app must have been found earlier when looking it up for the call,
74+
// so this must have been an internal error of some kind.
75+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
76+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
77+
}
78+
79+
#[tokio::test]
80+
async fn cell_not_found() {
81+
let mut app_call = MockAppCall::new();
82+
app_call
83+
.expect_handle_zome_call()
84+
.returning(|_, _, _, _, _| {
85+
Box::pin(async move {
86+
Err(crate::HcHttpGatewayError::HolochainError(
87+
ConductorApiError::CellNotFound,
88+
))
89+
})
90+
});
91+
let router = create_test_router(app_call);
92+
let (status_code, body) = router
93+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
94+
.await;
95+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
96+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
97+
}
98+
99+
#[tokio::test]
100+
async fn external_api_wire_error() {
101+
let mut app_call = MockAppCall::new();
102+
app_call
103+
.expect_handle_zome_call()
104+
.returning(|_, _, _, _, _| {
105+
Box::pin(async move {
106+
Err(crate::HcHttpGatewayError::HolochainError(
107+
ConductorApiError::ExternalApiWireError(
108+
ExternalApiWireError::ZomeCallUnauthorized("unauthorized".to_string()),
109+
),
110+
))
111+
})
112+
});
113+
let router = create_test_router(app_call);
114+
let (status_code, body) = router
115+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
116+
.await;
117+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
118+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
119+
}
120+
121+
#[tokio::test]
122+
async fn fresh_nonce_error() {
123+
let mut app_call = MockAppCall::new();
124+
app_call
125+
.expect_handle_zome_call()
126+
.returning(|_, _, _, _, _| {
127+
Box::pin(async move {
128+
Err(crate::HcHttpGatewayError::HolochainError(
129+
ConductorApiError::FreshNonceError("nonce_kaputt".into()),
130+
))
131+
})
132+
});
133+
let router = create_test_router(app_call);
134+
let (status_code, body) = router
135+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
136+
.await;
137+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
138+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
139+
}
140+
141+
#[tokio::test]
142+
async fn io_error() {
143+
let mut app_call = MockAppCall::new();
144+
app_call
145+
.expect_handle_zome_call()
146+
.returning(|_, _, _, _, _| {
147+
Box::pin(async move {
148+
Err(crate::HcHttpGatewayError::HolochainError(
149+
ConductorApiError::IoError(std::io::Error::other("ssd not found")),
150+
))
151+
})
152+
});
153+
let router = create_test_router(app_call);
154+
let (status_code, body) = router
155+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
156+
.await;
157+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
158+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
159+
}
160+
161+
#[tokio::test]
162+
async fn sign_zome_call_error() {
163+
let mut app_call = MockAppCall::new();
164+
app_call
165+
.expect_handle_zome_call()
166+
.returning(|_, _, _, _, _| {
167+
Box::pin(async move {
168+
Err(crate::HcHttpGatewayError::HolochainError(
169+
ConductorApiError::SignZomeCallError("unsigned".to_string()),
170+
))
171+
})
172+
});
173+
let router = create_test_router(app_call);
174+
let (status_code, body) = router
175+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
176+
.await;
177+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
178+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
179+
}
180+
181+
#[tokio::test]
182+
async fn websocket_error() {
183+
let mut app_call = MockAppCall::new();
184+
app_call
185+
.expect_handle_zome_call()
186+
.returning(|_, _, _, _, _| {
187+
Box::pin(async move {
188+
Err(crate::HcHttpGatewayError::HolochainError(
189+
ConductorApiError::WebsocketError(
190+
// WebsocketError is not exposed.
191+
std::io::Error::other("websocket closed").into(),
192+
),
193+
))
194+
})
195+
});
196+
let router = create_test_router(app_call);
197+
let (status_code, body) = router
198+
.request(&format!("/{DNA_HASH}/{APP_ID}/coordinator/fn_name"))
199+
.await;
200+
assert_eq!(status_code, StatusCode::INTERNAL_SERVER_ERROR);
201+
assert_eq!(body, r#"{"error":"Something went wrong"}"#);
202+
}

src/routes/zome_call/tests/validations.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::DNA_HASH;
12
use crate::test::router::TestRouter;
23
use crate::test::test_tracing::initialize_testing_tracing_subscriber;
34
use crate::{
@@ -9,9 +10,6 @@ use reqwest::StatusCode;
910
use std::collections::HashMap;
1011
use std::net::{Ipv4Addr, SocketAddr};
1112

12-
// DnaHash::from_raw_32(vec![1; 32]).to_string()
13-
const DNA_HASH: &str = "uhC0kAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQF-z86-";
14-
1513
#[tokio::test]
1614
async fn valid_dna_hash_is_accepted() {
1715
initialize_testing_tracing_subscriber();
@@ -101,7 +99,7 @@ async fn unauthorized_function_name_is_rejected() {
10199
assert_eq!(
102100
body,
103101
format!(
104-
r#"{{"error":"Function {fn_name} in zome zome_name in app coordinator is not allowed"}}"#
102+
r#"{{"error":"Function {fn_name} in zome zome_name in app coordinator is not authorized"}}"#
105103
)
106104
);
107105
}

0 commit comments

Comments
 (0)