Skip to content

Commit 35bd816

Browse files
authored
Merge pull request #343 from input-output-hk/lowhung/313-query-error-type
Refactor: Introduce standardized `RESTError` and `QueryError` types
2 parents ea66859 + 4675321 commit 35bd816

File tree

16 files changed

+1033
-1622
lines changed

16 files changed

+1033
-1622
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
# Pull Request Title
2+
23
<!--
34
Use a short, descriptive title:
45
Example: `node: fix crash when config is absent`
56
-->
67

78
## Description
9+
810
<!--
911
Describe what this PR does and why. Keep it concise but include enough context
1012
for reviewers who are not familiar with the area.
1113
-->
1214

1315
## Related Issue(s)
16+
1417
<!--
1518
Link any related issues, e.g. `Fixes #123` or `Relates to #456`.
1619
-->
1720

1821
## How was this tested?
22+
1923
<!--
2024
Describe the tests that you ran to verify your changes. Include instructions
2125
so reviewers can reproduce. Examples:
@@ -25,18 +29,21 @@ so reviewers can reproduce. Examples:
2529
-->
2630

2731
## Checklist
32+
2833
- [ ] My code builds and passes local tests
2934
- [ ] I added/updated tests for my changes, where applicable
3035
- [ ] I updated documentation (if applicable)
3136
- [ ] CI is green for this PR
3237

3338
## Impact / Side effects
39+
3440
<!--
3541
Describe any potential side effects, e.g. performance, compatibility, or security concerns.
3642
If the PR introduces a breaking change, explain migration steps for users.
3743
-->
3844

3945
## Reviewer notes / Areas to focus
46+
4047
<!--
4148
If you want specific feedback, list files/functions to review or aspects you are unsure about.
4249
-->

common/src/queries/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,11 @@ impl QueryError {
5656
}
5757
}
5858
}
59+
60+
impl From<anyhow::Error> for QueryError {
61+
fn from(err: anyhow::Error) -> Self {
62+
Self::Internal {
63+
message: err.to_string(),
64+
}
65+
}
66+
}

common/src/queries/utils.rs

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,45 @@
1-
use anyhow::Result;
21
use caryatid_sdk::Context;
32
use serde::Serialize;
43
use std::sync::Arc;
54

65
use crate::messages::{Message, RESTResponse};
6+
use crate::queries::errors::QueryError;
7+
use crate::rest_error::RESTError;
78

89
pub async fn query_state<T, F>(
910
context: &Arc<Context<Message>>,
1011
topic: &str,
1112
request_msg: Arc<Message>,
1213
extractor: F,
13-
) -> Result<T, anyhow::Error>
14+
) -> Result<T, QueryError>
1415
where
15-
F: FnOnce(Message) -> Result<T, anyhow::Error>,
16+
F: FnOnce(Message) -> Result<T, QueryError>,
1617
{
17-
// build message to query
1818
let raw_msg = context.message_bus.request(topic, request_msg).await?;
19-
2019
let message = Arc::try_unwrap(raw_msg).unwrap_or_else(|arc| (*arc).clone());
2120

2221
extractor(message)
2322
}
2423

25-
/// The outer option in the extractor return value is whether the response was handled by F
2624
pub async fn rest_query_state<T, F>(
2725
context: &Arc<Context<Message>>,
2826
topic: &str,
2927
request_msg: Arc<Message>,
3028
extractor: F,
31-
) -> Result<RESTResponse>
29+
) -> Result<RESTResponse, RESTError>
3230
where
33-
F: FnOnce(Message) -> Option<Result<Option<T>, anyhow::Error>>,
31+
F: FnOnce(Message) -> Option<Result<T, QueryError>>,
3432
T: Serialize,
3533
{
36-
let result = query_state(context, topic, request_msg, |response| {
37-
match extractor(response) {
38-
Some(response) => response,
39-
None => Err(anyhow::anyhow!(
34+
let data = query_state(context, topic, request_msg, |response| {
35+
extractor(response).ok_or_else(|| {
36+
QueryError::internal_error(format!(
4037
"Unexpected response message type while calling {topic}"
41-
)),
42-
}
38+
))
39+
})?
4340
})
44-
.await;
45-
match result {
46-
Ok(result) => match result {
47-
Some(result) => match serde_json::to_string(&result) {
48-
Ok(json) => Ok(RESTResponse::with_json(200, &json)),
49-
Err(e) => Ok(RESTResponse::with_text(
50-
500,
51-
&format!("Internal server error while calling {topic}: {e}"),
52-
)),
53-
},
54-
None => Ok(RESTResponse::with_text(404, "Not found")),
55-
},
56-
Err(e) => Ok(RESTResponse::with_text(
57-
500,
58-
&format!("Internal server error while calling {topic}: {e}"),
59-
)),
60-
}
41+
.await?;
42+
43+
let json = serde_json::to_string_pretty(&data)?;
44+
Ok(RESTResponse::with_json(200, &json))
6145
}

common/src/rest_error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ impl RESTError {
4242

4343
/// Parameter missing error
4444
pub fn param_missing(param_name: &str) -> Self {
45-
RESTError::BadRequest(format!("{} parameter is missing", param_name))
45+
RESTError::BadRequest(format!("Missing {} parameter", param_name))
4646
}
4747

4848
/// Invalid parameter error
4949
pub fn invalid_param(param_name: &str, reason: &str) -> Self {
50-
RESTError::BadRequest(format!("Invalid {}: {}", param_name, reason))
50+
RESTError::BadRequest(format!("Invalid {} parameter: {}", param_name, reason))
5151
}
5252

5353
/// Invalid hex string error

common/src/rest_helper.rs

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Helper functions for REST handlers
22
33
use crate::messages::{Message, RESTResponse};
4+
use crate::rest_error::RESTError;
45
use anyhow::{anyhow, Result};
56
use caryatid_sdk::Context;
67
use futures::future::Future;
@@ -17,20 +18,15 @@ pub fn handle_rest<F, Fut>(
1718
) -> JoinHandle<()>
1819
where
1920
F: Fn() -> Fut + Send + Sync + Clone + 'static,
20-
Fut: Future<Output = Result<RESTResponse>> + Send + 'static,
21+
Fut: Future<Output = Result<RESTResponse, RESTError>> + Send + 'static,
2122
{
2223
context.handle(topic, move |message: Arc<Message>| {
2324
let handler = handler.clone();
2425
async move {
2526
let response = match message.as_ref() {
2627
Message::RESTRequest(request) => {
2728
info!("REST received {} {}", request.method, request.path);
28-
match handler().await {
29-
Ok(response) => response,
30-
Err(error) => {
31-
RESTResponse::with_text(500, &format!("{error:?}").to_string())
32-
}
33-
}
29+
handler().await.unwrap_or_else(|error| error.into())
3430
}
3531
_ => {
3632
error!("Unexpected message type {:?}", message);
@@ -51,7 +47,7 @@ pub fn handle_rest_with_path_parameter<F, Fut>(
5147
) -> JoinHandle<()>
5248
where
5349
F: Fn(&[&str]) -> Fut + Send + Sync + Clone + 'static,
54-
Fut: Future<Output = Result<RESTResponse>> + Send + 'static,
50+
Fut: Future<Output = Result<RESTResponse, RESTError>> + Send + 'static,
5551
{
5652
let topic_owned = topic.to_string();
5753
context.handle(topic, move |message: Arc<Message>| {
@@ -65,12 +61,7 @@ where
6561
extract_params_from_topic_and_path(&topic_owned, &request.path_elements);
6662
let params_slice: Vec<&str> = params_vec.iter().map(|s| s.as_str()).collect();
6763

68-
match handler(&params_slice).await {
69-
Ok(response) => response,
70-
Err(error) => {
71-
RESTResponse::with_text(500, &format!("{error:?}").to_string())
72-
}
73-
}
64+
handler(&params_slice).await.unwrap_or_else(|error| error.into())
7465
}
7566
_ => {
7667
error!("Unexpected message type {:?}", message);
@@ -83,26 +74,23 @@ where
8374
})
8475
}
8576

86-
// Handle a REST request with query parameters
77+
/// Handle a REST request with query parameters
8778
pub fn handle_rest_with_query_parameters<F, Fut>(
8879
context: Arc<Context<Message>>,
8980
topic: &str,
9081
handler: F,
9182
) -> JoinHandle<()>
9283
where
9384
F: Fn(HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
94-
Fut: Future<Output = Result<RESTResponse>> + Send + 'static,
85+
Fut: Future<Output = Result<RESTResponse, RESTError>> + Send + 'static,
9586
{
9687
context.handle(topic, move |message: Arc<Message>| {
9788
let handler = handler.clone();
9889
async move {
9990
let response = match message.as_ref() {
10091
Message::RESTRequest(request) => {
10192
let params = request.query_parameters.clone();
102-
match handler(params).await {
103-
Ok(response) => response,
104-
Err(error) => RESTResponse::with_text(500, &format!("{error:?}")),
105-
}
93+
handler(params).await.unwrap_or_else(|error| error.into())
10694
}
10795
_ => RESTResponse::with_text(500, "Unexpected message in REST request"),
10896
};
@@ -112,15 +100,15 @@ where
112100
})
113101
}
114102

115-
// Handle a REST request with path and query parameters
103+
/// Handle a REST request with path and query parameters
116104
pub fn handle_rest_with_path_and_query_parameters<F, Fut>(
117105
context: Arc<Context<Message>>,
118106
topic: &str,
119107
handler: F,
120108
) -> JoinHandle<()>
121109
where
122110
F: Fn(&[&str], HashMap<String, String>) -> Fut + Send + Sync + Clone + 'static,
123-
Fut: Future<Output = Result<RESTResponse>> + Send + 'static,
111+
Fut: Future<Output = Result<RESTResponse, RESTError>> + Send + 'static,
124112
{
125113
let topic_owned = topic.to_string();
126114
context.handle(topic, move |message: Arc<Message>| {
@@ -133,10 +121,7 @@ where
133121
extract_params_from_topic_and_path(&topic_owned, &request.path_elements);
134122
let params_slice: Vec<&str> = params_vec.iter().map(|s| s.as_str()).collect();
135123
let query_params = request.query_parameters.clone();
136-
match handler(&params_slice, query_params).await {
137-
Ok(response) => response,
138-
Err(error) => RESTResponse::with_text(500, &format!("{error:?}")),
139-
}
124+
handler(&params_slice, query_params).await.unwrap_or_else(|error| error.into())
140125
}
141126
_ => RESTResponse::with_text(500, "Unexpected message in REST request"),
142127
};

common/src/snapshot/NOTES.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Bootstrapping from a Snapshot file
2+
23
We can boot an Acropolis node either from geneis and replay all of the blocks up to
34
some point, or we can boot from a snapshot file. This module provides the components
4-
needed to boot from a snapshot file. See [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) for the process that references and runs with these helpers.
5+
needed to boot from a snapshot file.
6+
See [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) for the process that
7+
references and runs with these helpers.
58

69
Booting from a snapshot takes minutes instead of the hours it takes to boot from
710
genesis. It also allows booting from a given epoch which allows one to create tests
@@ -10,22 +13,27 @@ eras and will typically boot from Conway around epoch 305, 306, and 307. It take
1013
three epochs to have enough context to correctly calculate the rewards.
1114

1215
The required data for boostrapping are:
16+
1317
- snapshot files (each has an associated epoch number and point)
1418
- nonces
1519
- headers
1620

1721
## Snapshot Files
22+
1823
The snapshots come from the Amaru project. In their words,
1924
"the snapshots we generated are different [from a Mithril snapshot]: they're
20-
the actual ledger state; i.e. the in-memory state that is constructed by iterating over each block up to a specific point. So, it's all the UTxOs, the set of pending governance actions, the account balance, etc.
25+
the actual ledger state; i.e. the in-memory state that is constructed by iterating over each block up to a specific
26+
point. So, it's all the UTxOs, the set of pending governance actions, the account balance, etc.
2127
If you get this from a trusted source, you don't need to do any replay, you can just start up and load this from disk.
22-
The format of these is completely non-standard; we just forked the haskell node and spit out whatever we needed to in CBOR."
28+
The format of these is completely non-standard; we just forked the haskell node and spit out whatever we needed to in
29+
CBOR."
2330

2431
Snapshot files are referenced by their epoch number in the config.json file below.
2532

2633
See [Amaru snapshot format](../../../docs/amaru-snapshot-structure.md)
2734

2835
## Configuration files
36+
2937
There is a path for each network bootstrap configuration file. Network Should
3038
be one of 'mainnet', 'preprod', 'preview' or 'testnet_<magic>' where
3139
`magic` is a 32-bits unsigned value denoting a particular testnet.
@@ -43,7 +51,8 @@ a network name of `preview`, the expected layout for configuration files would b
4351
* `data/preview/nonces.json`: a list of `InitialNonces` values,
4452
* `data/preview/headers.json`: a list of `Point`s.
4553

46-
These files are loaded by [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) during bootup.
54+
These files are loaded by [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs)
55+
during bootup.
4756

4857
## Bootstrapping sequence
4958

@@ -75,7 +84,8 @@ headers. Snapshot parsing is done while streaming the data to keep the memory
7584
footprint lower. As elements of the file are parsed, callbacks provide the data
7685
to the boostrapper which publishes the data on the message bus.
7786

78-
There are TODO markers in [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs) that show where to add the
87+
There are TODO markers in [snapshot_bootsrapper](../../../modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs)
88+
that show where to add the
7989
publishing of the parsed snapshot data.
8090

8191

modules/drdd_state/src/rest.rs

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::state::State;
2+
use acropolis_common::rest_error::RESTError;
23
use acropolis_common::{extract_strict_query_params, messages::RESTResponse, DRepCredential};
3-
use anyhow::Result;
44
use serde::Serialize;
55
use std::{collections::HashMap, sync::Arc};
66
use tokio::sync::Mutex;
@@ -17,16 +17,10 @@ struct DRDDResponse {
1717
pub async fn handle_drdd(
1818
state: Option<Arc<Mutex<State>>>,
1919
params: HashMap<String, String>,
20-
) -> Result<RESTResponse> {
21-
let locked = match state.as_ref() {
22-
Some(state) => state.lock().await,
23-
None => {
24-
return Ok(RESTResponse::with_text(
25-
503,
26-
"DRDD storage is disabled by configuration",
27-
));
28-
}
29-
};
20+
) -> Result<RESTResponse, RESTError> {
21+
let state_arc = state.as_ref().ok_or_else(|| RESTError::storage_disabled("DRDD"))?;
22+
23+
let locked = state_arc.lock().await;
3024

3125
extract_strict_query_params!(params, {
3226
"epoch" => epoch: Option<u64>,
@@ -36,10 +30,7 @@ pub async fn handle_drdd(
3630
Some(epoch) => match locked.get_epoch(epoch) {
3731
Some(drdd) => Some(drdd),
3832
None => {
39-
return Ok(RESTResponse::with_text(
40-
404,
41-
&format!("DRDD not found for epoch {}", epoch),
42-
));
33+
return Err(RESTError::not_found(&format!("DRDD in epoch {}", epoch)));
4334
}
4435
},
4536
None => locked.get_latest(),
@@ -65,26 +56,16 @@ pub async fn handle_drdd(
6556
no_confidence: drdd.no_confidence,
6657
};
6758

68-
match serde_json::to_string(&response) {
69-
Ok(body) => Ok(RESTResponse::with_json(200, &body)),
70-
Err(e) => Ok(RESTResponse::with_text(
71-
500,
72-
&format!("Internal server error retrieving DRep delegation distribution: {e}"),
73-
)),
74-
}
59+
let body = serde_json::to_string(&response)?;
60+
Ok(RESTResponse::with_json(200, &body))
7561
} else {
7662
let response = DRDDResponse {
7763
dreps: HashMap::new(),
7864
abstain: 0,
7965
no_confidence: 0,
8066
};
8167

82-
match serde_json::to_string(&response) {
83-
Ok(body) => Ok(RESTResponse::with_json(200, &body)),
84-
Err(_) => Ok(RESTResponse::with_text(
85-
500,
86-
"Internal server error serializing empty DRDD response",
87-
)),
88-
}
68+
let body = serde_json::to_string(&response)?;
69+
Ok(RESTResponse::with_json(200, &body))
8970
}
9071
}

0 commit comments

Comments
 (0)