Skip to content

Commit 2d908cd

Browse files
feat(rover-client): improves error handling
Adds some more strongly typed errors in rover-client in addition to some new suggestions in rover.
1 parent 616ae59 commit 2d908cd

File tree

17 files changed

+175
-85
lines changed

17 files changed

+175
-85
lines changed

crates/rover-client/src/blocking/client.rs

+2-7
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,7 @@ impl Client {
4646
pub fn handle_response<Q: graphql_client::GraphQLQuery>(
4747
response: reqwest::blocking::Response,
4848
) -> Result<Q::ResponseData, RoverClientError> {
49-
let response_body: graphql_client::Response<Q::ResponseData> =
50-
response
51-
.json()
52-
.map_err(|_| RoverClientError::HandleResponse {
53-
msg: String::from("failed to parse response JSON"),
54-
})?;
49+
let response_body: graphql_client::Response<Q::ResponseData> = response.json()?;
5550

5651
if let Some(errs) = response_body.errors {
5752
return Err(RoverClientError::GraphQL {
@@ -66,7 +61,7 @@ impl Client {
6661
if let Some(data) = response_body.data {
6762
Ok(data)
6863
} else {
69-
Err(RoverClientError::NoData)
64+
Err(RoverClientError::MalformedResponse)
7065
}
7166
}
7267
}

crates/rover-client/src/error.rs

+39-20
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ use thiserror::Error;
44
#[derive(Error, Debug)]
55
pub enum RoverClientError {
66
/// The provided GraphQL was invalid.
7-
#[error("encountered a GraphQL error, registry responded with: {msg}")]
8-
GraphQL { msg: String },
7+
#[error("GraphQL error:\n {msg}")]
8+
GraphQL {
9+
/// The encountered GraphQL error.
10+
msg: String,
11+
},
912

1013
/// Tried to build a [HeaderMap] with an invalid header name.
1114
#[error("invalid header name")]
@@ -20,37 +23,53 @@ pub enum RoverClientError {
2023
InvalidJSON(#[from] serde_json::Error),
2124

2225
/// Encountered an error handling the received response.
23-
#[error("encountered an error handling the response: {msg}")]
24-
HandleResponse {
26+
#[error("{msg}")]
27+
AdhocError {
2528
/// The error message.
2629
msg: String,
2730
},
2831

32+
/// The user provided an invalid subgraph name.
33+
#[error("Could not find subgraph \"{invalid_subgraph}\".")]
34+
NoSubgraphInGraph {
35+
/// The invalid subgraph name
36+
invalid_subgraph: String,
37+
38+
/// A list of valid subgraph names
39+
// this is not used in the error message, but can be accessed
40+
// by application-level error handlers
41+
valid_subgraphs: Vec<String>,
42+
},
43+
44+
/// The Studio API could not find a variant for a graph
45+
#[error(
46+
"The graph registry does not contain variant \"{invalid_variant}\" for graph \"{graph}\""
47+
)]
48+
NoSchemaForVariant {
49+
/// The name of the graph.
50+
graph: String,
51+
52+
/// The non-existent variant.
53+
invalid_variant: String,
54+
},
55+
2956
/// Encountered an error sending the request.
3057
#[error("encountered an error while sending a request")]
3158
SendRequest(#[from] reqwest::Error),
3259

33-
/// This error occurs when there are no `body.errors` but `body.data` is
34-
/// also empty. In proper GraphQL responses, there should _always_ be either
35-
/// body.errors or body.data
36-
#[error("The response from the server was malformed. There was no data found in the reponse body. This is likely an error in GraphQL execution")]
37-
NoData,
38-
3960
/// when someone provides a bad graph/variant combination or isn't
4061
/// validated properly, we don't know which reason is at fault for data.service
4162
/// being empty, so this error tells them to check both.
42-
#[error("No graph found. Either the graph@variant combination wasn't found or your API key is invalid.")]
43-
NoService,
63+
#[error("Could not find graph with name \"{graph}\"")]
64+
NoService { graph: String },
4465

45-
/// This error occurs when the Studio API returns no composition errors AND
46-
/// no check result. This response shouldn't be possible!
47-
#[error(
48-
"The response from the server was malformed, there was no data from the check operation."
49-
)]
50-
NoCheckData,
66+
/// This error occurs when the Studio API returns no implementing services for a graph
67+
/// This response shouldn't be possible!
68+
#[error("The response from Apollo Studio was malformed.")]
69+
MalformedResponse,
5170

52-
#[error("The graph `{graph_name}` is a non-federated graph. This operation is only possible for federated graphs")]
53-
ExpectedFederatedGraph { graph_name: String },
71+
#[error("The graph `{graph}` is a non-federated graph. This operation is only possible for federated graphs")]
72+
ExpectedFederatedGraph { graph: String },
5473

5574
/// The API returned an invalid ChangeSeverity value
5675
#[error("Invalid ChangeSeverity.")]

crates/rover-client/src/query/graph/check.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ pub fn run(
2525
variables: check_schema_query::Variables,
2626
client: &StudioClient,
2727
) -> Result<CheckResponse, RoverClientError> {
28+
let graph = variables.graph_id.clone();
2829
let data = client.post::<CheckSchemaQuery>(variables)?;
29-
get_check_response_from_data(data)
30+
get_check_response_from_data(data, graph)
3031
}
3132

3233
#[derive(Debug)]
@@ -39,8 +40,9 @@ pub struct CheckResponse {
3940

4041
fn get_check_response_from_data(
4142
data: check_schema_query::ResponseData,
43+
graph: String,
4244
) -> Result<CheckResponse, RoverClientError> {
43-
let service = data.service.ok_or(RoverClientError::NoService)?;
45+
let service = data.service.ok_or(RoverClientError::NoService { graph })?;
4446
let target_url = get_url(service.check_schema.target_url);
4547

4648
let diff_to_previous = service.check_schema.diff_to_previous;

crates/rover-client/src/query/graph/fetch.rs

+24-7
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,34 @@ pub fn run(
2626
variables: fetch_schema_query::Variables,
2727
client: &StudioClient,
2828
) -> Result<String, RoverClientError> {
29+
let graph = variables.graph_id.clone();
30+
let invalid_variant = variables
31+
.variant
32+
.clone()
33+
.unwrap_or_else(|| "current".to_string());
2934
let response_data = client.post::<FetchSchemaQuery>(variables)?;
30-
get_schema_from_response_data(response_data)
35+
get_schema_from_response_data(response_data, graph, invalid_variant)
3136
// if we want json, we can parse & serialize it here
3237
}
3338

3439
fn get_schema_from_response_data(
3540
response_data: fetch_schema_query::ResponseData,
41+
graph: String,
42+
invalid_variant: String,
3643
) -> Result<String, RoverClientError> {
3744
let service_data = match response_data.service {
3845
Some(data) => Ok(data),
39-
None => Err(RoverClientError::NoService),
46+
None => Err(RoverClientError::NoService {
47+
graph: graph.clone(),
48+
}),
4049
}?;
4150

4251
if let Some(schema) = service_data.schema {
4352
Ok(schema.document)
4453
} else {
45-
Err(RoverClientError::HandleResponse {
46-
msg: "No schema found for this variant".to_string(),
54+
Err(RoverClientError::NoSchemaForVariant {
55+
graph,
56+
invalid_variant,
4757
})
4858
}
4959
}
@@ -62,7 +72,8 @@ mod tests {
6272
}
6373
});
6474
let data: fetch_schema_query::ResponseData = serde_json::from_value(json_response).unwrap();
65-
let output = get_schema_from_response_data(data);
75+
let (graph, invalid_variant) = mock_vars();
76+
let output = get_schema_from_response_data(data, graph, invalid_variant);
6677

6778
assert!(output.is_ok());
6879
assert_eq!(output.unwrap(), "type Query { hello: String }".to_string());
@@ -72,7 +83,8 @@ mod tests {
7283
fn get_schema_from_response_data_errs_on_no_service() {
7384
let json_response = json!({ "service": null });
7485
let data: fetch_schema_query::ResponseData = serde_json::from_value(json_response).unwrap();
75-
let output = get_schema_from_response_data(data);
86+
let (graph, invalid_variant) = mock_vars();
87+
let output = get_schema_from_response_data(data, graph, invalid_variant);
7688

7789
assert!(output.is_err());
7890
}
@@ -85,8 +97,13 @@ mod tests {
8597
}
8698
});
8799
let data: fetch_schema_query::ResponseData = serde_json::from_value(json_response).unwrap();
88-
let output = get_schema_from_response_data(data);
100+
let (graph, invalid_variant) = mock_vars();
101+
let output = get_schema_from_response_data(data, graph, invalid_variant);
89102

90103
assert!(output.is_err());
91104
}
105+
106+
fn mock_vars() -> (String, String) {
107+
("mygraph".to_string(), "current".to_string())
108+
}
92109
}

crates/rover-client/src/query/graph/push.rs

+12-7
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,27 @@ pub fn run(
2828
variables: push_schema_mutation::Variables,
2929
client: &StudioClient,
3030
) -> Result<PushResponse, RoverClientError> {
31+
let graph = variables.graph_id.clone();
3132
let data = client.post::<PushSchemaMutation>(variables)?;
32-
let push_response = get_push_response_from_data(data)?;
33+
let push_response = get_push_response_from_data(data, graph)?;
3334
build_response(push_response)
3435
}
3536

3637
fn get_push_response_from_data(
3738
data: push_schema_mutation::ResponseData,
39+
graph: String,
3840
) -> Result<push_schema_mutation::PushSchemaMutationServiceUploadSchema, RoverClientError> {
3941
// then, from the response data, get .service?.upload_schema?
4042
let service_data = match data.service {
4143
Some(data) => data,
42-
None => return Err(RoverClientError::NoService),
44+
None => return Err(RoverClientError::NoService { graph }),
4345
};
4446

4547
if let Some(opt_data) = service_data.upload_schema {
4648
Ok(opt_data)
4749
} else {
48-
Err(RoverClientError::HandleResponse {
49-
msg: "No response from mutation. Check your API key & graph name".to_string(),
50+
Err(RoverClientError::AdhocError {
51+
msg: "No response from the graph registry. Check your API key & graph name".to_string(),
5052
})
5153
}
5254
}
@@ -56,15 +58,18 @@ fn build_response(
5658
) -> Result<PushResponse, RoverClientError> {
5759
if !push_response.success {
5860
let msg = format!("Schema upload failed with error: {}", push_response.message);
59-
return Err(RoverClientError::HandleResponse { msg });
61+
return Err(RoverClientError::AdhocError { msg });
6062
}
6163

6264
let hash = match &push_response.tag {
6365
// we only want to print the first 6 chars of a hash
6466
Some(tag_data) => tag_data.schema.hash.clone()[..6].to_string(),
6567
None => {
66-
let msg = format!("No schema tag info available ({})", push_response.message);
67-
return Err(RoverClientError::HandleResponse { msg });
68+
let msg = format!(
69+
"No data in response from schema push. Failed with message: {}",
70+
push_response.message
71+
);
72+
return Err(RoverClientError::AdhocError { msg });
6873
}
6974
};
7075

crates/rover-client/src/query/subgraph/check.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ pub fn run(
2525
variables: check_partial_schema_query::Variables,
2626
client: &StudioClient,
2727
) -> Result<CheckResponse, RoverClientError> {
28+
let graph = variables.graph_id.clone();
2829
let data = client.post::<CheckPartialSchemaQuery>(variables)?;
29-
get_check_response_from_data(data)
30+
get_check_response_from_data(data, graph)
3031
}
3132

3233
pub enum CheckResponse {
@@ -44,8 +45,9 @@ pub struct CheckResult {
4445

4546
fn get_check_response_from_data(
4647
data: check_partial_schema_query::ResponseData,
48+
graph: String,
4749
) -> Result<CheckResponse, RoverClientError> {
48-
let service = data.service.ok_or(RoverClientError::NoService)?;
50+
let service = data.service.ok_or(RoverClientError::NoService { graph })?;
4951

5052
// for some reason this is a `Vec<Option<CompositionError>>`
5153
// we convert this to just `Vec<CompositionError>` because the `None`
@@ -60,7 +62,7 @@ fn get_check_response_from_data(
6062
let check_schema_result = service
6163
.check_partial_schema
6264
.check_schema_result
63-
.ok_or(RoverClientError::NoCheckData)?;
65+
.ok_or(RoverClientError::MalformedResponse)?;
6466

6567
let target_url = get_url(check_schema_result.target_url);
6668

crates/rover-client/src/query/subgraph/delete.graphql

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
mutation DeleteServiceMutation(
2-
$id: ID!
2+
$graphId: ID!
33
$graphVariant: String!
44
$name: String!
55
$dryRun: Boolean!
66
) {
7-
service(id: $id) {
7+
service(id: $graphId) {
88
removeImplementingServiceAndTriggerComposition(
99
graphVariant: $graphVariant
1010
name: $name

crates/rover-client/src/query/subgraph/delete.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,19 @@ pub fn run(
3333
variables: delete_service_mutation::Variables,
3434
client: &StudioClient,
3535
) -> Result<DeleteServiceResponse, RoverClientError> {
36+
let graph = variables.graph_id.clone();
3637
let response_data = client.post::<DeleteServiceMutation>(variables)?;
37-
let data = get_delete_data_from_response(response_data)?;
38+
let data = get_delete_data_from_response(response_data, graph)?;
3839
Ok(build_response(data))
3940
}
4041

4142
fn get_delete_data_from_response(
4243
response_data: delete_service_mutation::ResponseData,
44+
graph: String,
4345
) -> Result<RawMutationResponse, RoverClientError> {
4446
let service_data = match response_data.service {
4547
Some(data) => Ok(data),
46-
None => Err(RoverClientError::NoService),
48+
None => Err(RoverClientError::NoService { graph }),
4749
}?;
4850

4951
Ok(service_data.remove_implementing_service_and_trigger_composition)

crates/rover-client/src/query/subgraph/fetch.rs

+13-13
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,23 @@ pub fn run(
2424
// operation response by this name
2525
subgraph: &str,
2626
) -> Result<String, RoverClientError> {
27-
let graph_name = variables.graph_id.clone();
27+
let graph = variables.graph_id.clone();
2828
let response_data = client.post::<FetchSubgraphQuery>(variables)?;
29-
let services = get_services_from_response_data(response_data, &graph_name)?;
29+
let services = get_services_from_response_data(response_data, graph)?;
3030
get_sdl_for_service(services, subgraph)
3131
// if we want json, we can parse & serialize it here
3232
}
3333

3434
type ServiceList = Vec<fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServicesOnFederatedImplementingServicesServices>;
3535
fn get_services_from_response_data(
3636
response_data: fetch_subgraph_query::ResponseData,
37-
graph_name: &str,
37+
graph: String,
3838
) -> Result<ServiceList, RoverClientError> {
3939
let service_data = match response_data.service {
4040
Some(data) => Ok(data),
41-
None => Err(RoverClientError::NoService),
41+
None => Err(RoverClientError::NoService {
42+
graph: graph.clone(),
43+
}),
4244
}?;
4345

4446
// get list of services
@@ -49,7 +51,7 @@ fn get_services_from_response_data(
4951
// of a non-federated graph. Fow now, this case still exists, but
5052
// wont' for long. Check on this later (Jake) :)
5153
None => Err(RoverClientError::ExpectedFederatedGraph {
52-
graph_name: graph_name.to_string(),
54+
graph: graph.clone(),
5355
}),
5456
}?;
5557

@@ -58,7 +60,7 @@ fn get_services_from_response_data(
5860
Ok(services.services)
5961
},
6062
fetch_subgraph_query::FetchSubgraphQueryServiceImplementingServices::NonFederatedImplementingService => {
61-
Err(RoverClientError::ExpectedFederatedGraph { graph_name: graph_name.to_string() })
63+
Err(RoverClientError::ExpectedFederatedGraph { graph })
6264
}
6365
}
6466
}
@@ -72,14 +74,12 @@ fn get_sdl_for_service(services: ServiceList, subgraph: &str) -> Result<String,
7274
if let Some(service) = service {
7375
Ok(service.active_partial_schema.sdl.clone())
7476
} else {
75-
let all_supgraph_names: Vec<String> = services.iter().map(|svc| svc.name.clone()).collect();
76-
let msg = format!(
77-
"Could not find subgraph `{}` in list of subgraphs. Available subgraphs to fetch: [{}]",
78-
subgraph,
79-
all_supgraph_names.join(", ")
80-
);
77+
let valid_subgraphs: Vec<String> = services.iter().map(|svc| svc.name.clone()).collect();
8178

82-
Err(RoverClientError::HandleResponse { msg })
79+
Err(RoverClientError::NoSubgraphInGraph {
80+
invalid_subgraph: subgraph.to_string(),
81+
valid_subgraphs,
82+
})
8383
}
8484
}
8585

0 commit comments

Comments
 (0)