Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
bce1c22
WIP error counting pluging
rregitsky Apr 8, 2025
e0755fd
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Apr 24, 2025
c73714f
switch to submodule for telemetry plugin
rregitsky Apr 24, 2025
a61fcf7
fix err config passthrough
rregitsky Apr 24, 2025
6e69ef0
remove old fns and refs. Move unit tests
rregitsky Apr 24, 2025
b89166c
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Apr 24, 2025
cd6d85b
fix response borrowing and async
rregitsky Apr 24, 2025
ca64aa2
fix context move
rregitsky Apr 25, 2025
3baf0e0
use context keeping track of prev counts
rregitsky Apr 25, 2025
a2637db
moved count_graphql_error. existing unit tests passing.
rregitsky Apr 25, 2025
189f356
working count_errors test
rregitsky Apr 28, 2025
a6f3c00
count_errors test with prev counted
rregitsky Apr 28, 2025
a05630c
fn for each layer
rregitsky Apr 28, 2025
d18e614
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Apr 28, 2025
9574ae0
call counter for execution service
rregitsky Apr 29, 2025
07e74a2
lint fixes
rregitsky Apr 30, 2025
101b50c
add router service counter
rregitsky Apr 30, 2025
b258884
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Apr 30, 2025
d2e652f
remove now unused count_operation_error_codes
rregitsky Apr 30, 2025
ec4171c
inline count value completion errors
rregitsky Apr 30, 2025
64ec25e
Add uuid to gql error struct. Fix struct literals
rregitsky May 1, 2025
1db5e0a
store/pull router layer errors in/from context
rregitsky May 7, 2025
12a25c9
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky May 19, 2025
3aa06b5
new err default impl. Fix from_value
rregitsky May 21, 2025
3d1d6d2
Revert away from using builder everywhere
rregitsky May 21, 2025
bed6c13
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky May 21, 2025
90129d7
Revert "Revert away from using builder everywhere"
rregitsky May 21, 2025
dc4b371
Revert default. Make extension_code Option<String>
rregitsky May 21, 2025
f506777
fix one more error builder spot
rregitsky May 21, 2025
1aecdc6
Failed attempt converting to builder (to be reverted)`
rregitsky May 23, 2025
9c88ed7
convert all router constructors into builders
rregitsky Jun 3, 2025
54f1909
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Jun 3, 2025
cc4fd6c
fix comment
rregitsky Jun 3, 2025
8c1c4d7
Use apollo id for error counting
rregitsky Jun 4, 2025
e91ec86
fix test error comparisons
rregitsky Jun 4, 2025
50528ef
lint fixes
rregitsky Jun 4, 2025
b6ebe24
fix err counting tests
rregitsky Jun 4, 2025
03674a0
serde default random uuid. Skip serialize. Remove debug
rregitsky Jun 5, 2025
aba7324
fix auth tests
rregitsky Jun 5, 2025
85e76de
fix some tests
rregitsky Jun 6, 2025
956ddca
parts builder --> http::response builder to avoid changing response body
rregitsky Jun 9, 2025
c2ad7d0
Fix most remaining tests
rregitsky Jun 9, 2025
fdc7bba
lint fixes
rregitsky Jun 9, 2025
f915cff
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Jun 9, 2025
b38b8ce
WIP convert license unit test to integration test
rregitsky Jun 11, 2025
2b39930
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Jun 11, 2025
dd0d382
additional attempts to fix licesnse enforcement test
rregitsky Jun 12, 2025
87445fb
fix operations test and move to telemetry plugin
rregitsky Jun 13, 2025
b58dc6b
lint
rregitsky Jun 13, 2025
7ef5858
fix licesnse enforcement test
rregitsky Jun 13, 2025
127decc
fix sugraph convert errors to gql test
rregitsky Jun 13, 2025
8578da5
carry over apollo id after removing _entities
rregitsky Jun 13, 2025
496c33c
fix some integration tests
rregitsky Jun 16, 2025
98aff60
Merge remote-tracking branch 'origin/dev' into rreg/err_count_plugin_…
rregitsky Jun 16, 2025
41439ab
fix fleet detector test. Revert to parts builder
rregitsky Jun 16, 2025
b3c1c4e
lint
rregitsky Jun 16, 2025
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
33 changes: 18 additions & 15 deletions apollo-router/src/axum_factory/listeners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,14 +550,17 @@ mod tests {
.unwrap();

let endpoint = service_fn(|req: router::Request| async move {
Ok::<_, BoxError>(router::Response {
response: http::Response::builder()
.body::<crate::services::router::Body>(body::from_bytes(
"this is a test".to_string(),
))
Ok::<_, BoxError>(
router::Response::http_response_builder()
.response(
http::Response::builder().body::<crate::services::router::Body>(
body::from_bytes("this is a test".to_string()),
)?,
)
.context(req.context)
.build()
.unwrap(),
context: req.context,
})
)
})
.boxed();

Expand Down Expand Up @@ -591,14 +594,14 @@ mod tests {
.build()
.unwrap();
let endpoint = service_fn(|req: router::Request| async move {
Ok::<_, BoxError>(router::Response {
response: http::Response::builder()
.body::<crate::services::router::Body>(body::from_bytes(
"this is a test".to_string(),
))
.unwrap(),
context: req.context,
})
router::Response::http_response_builder()
.response(
http::Response::builder().body::<crate::services::router::Body>(
body::from_bytes("this is a test".to_string()),
)?,
)
.context(req.context)
.build()
})
.boxed();

Expand Down
22 changes: 12 additions & 10 deletions apollo-router/src/axum_factory/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,7 @@ async fn response_failure() -> Result<(), ApolloRouterError> {
.await;
let (server, client) = init(router_service).await;

let response = client
let mut response = client
.post(format!(
"{}/",
server.graphql_listen_address().as_ref().unwrap()
Expand All @@ -1066,15 +1066,17 @@ async fn response_failure() -> Result<(), ApolloRouterError> {
.await
.unwrap();

assert_eq!(
response,
crate::error::FetchError::SubrequestHttpError {
status_code: Some(200),
service: "Mock service".to_string(),
reason: "Mock error".to_string(),
}
.to_response()
);
let mut expected_response = crate::error::FetchError::SubrequestHttpError {
status_code: Some(200),
service: "Mock service".to_string(),
reason: "Mock error".to_string(),
}
.to_response();
// Overwrite error IDs to avoid random Uuid mismatch
response.errors[0] = response.errors[0].clone().with_null_id();
expected_response.errors[0] = expected_response.errors[0].clone().with_null_id();

assert_eq!(response, expected_response);
server.shutdown().await
}

Expand Down
4 changes: 4 additions & 0 deletions apollo-router/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub(crate) const OPERATION_NAME: &str = "apollo::supergraph::operation_name";
pub(crate) const OPERATION_KIND: &str = "apollo::supergraph::operation_kind";
/// The key to know if the response body contains at least 1 GraphQL error
pub(crate) const CONTAINS_GRAPHQL_ERROR: &str = "apollo::telemetry::contains_graphql_error";
/// The key to a map of errors that were already counted in a previous layer
pub(crate) const COUNTED_ERRORS: &str = "apollo::telemetry::counted_errors";
/// The key for the full list of errors in the router response. This allows us to pull the value in plugins without having to deserialize the router response.
pub(crate) const ROUTER_RESPONSE_ERRORS: &str = "apollo::router::response_errors";

pub(crate) use deprecated::context_key_from_deprecated;
pub(crate) use deprecated::context_key_to_deprecated;
Expand Down
17 changes: 10 additions & 7 deletions apollo-router/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@ impl FetchError {
}
}

Error {
message: self.to_string(),
locations: Default::default(),
path,
extensions: value.as_object().unwrap().to_owned(),
}
Error::builder()
.message(self.to_string())
.locations(Vec::default())
.and_path(path)
.extensions(value.as_object().unwrap().to_owned())
.build()
}

/// Convert the error to an appropriate response.
Expand Down Expand Up @@ -660,6 +660,9 @@ mod tests {
)
.build();

assert_eq!(expected_gql_error, error.to_graphql_error(None));
assert_eq!(
expected_gql_error.with_null_id(),
error.to_graphql_error(None).with_null_id()
);
}
}
95 changes: 78 additions & 17 deletions apollo-router/src/graphql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod visitor;

use std::fmt;
use std::pin::Pin;
use std::str::FromStr;

use apollo_compiler::response::GraphQLError as CompilerExecutionError;
use apollo_compiler::response::ResponseDataPathSegment;
Expand All @@ -20,6 +21,7 @@ use serde::Serialize;
use serde_json_bytes::ByteString;
use serde_json_bytes::Map as JsonMap;
use serde_json_bytes::Value;
use uuid::Uuid;
pub(crate) use visitor::ResponseVisitor;

use crate::json_ext::Object;
Expand Down Expand Up @@ -52,7 +54,7 @@ pub struct Location {
/// as may be found in the `errors` field of a GraphQL [`Response`].
///
/// Converted to (or from) JSON with serde.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Error {
Expand All @@ -70,6 +72,10 @@ pub struct Error {
/// The optional GraphQL extensions for this error.
#[serde(default, skip_serializing_if = "Object::is_empty")]
pub extensions: Object,

/// A unique identifier for this error
#[serde(default = "generate_uuid", skip_serializing)]
apollo_id: Uuid,
}
// Implement getter and getter_mut to not use pub field directly

Expand Down Expand Up @@ -103,25 +109,39 @@ impl Error {
/// Optional, may be called multiple times.
/// Adds one item to the [`Error::extensions`] map.
///
/// * `.extension_code(impl Into<`[`String`]`>)`
/// Optional.
/// Sets the "code" in the extension map. Will be ignored if extension already has this key
/// set.
///
/// * `.apollo_id(impl Into<`[`Uuid`]`>)`
/// Optional.
/// Sets the unique identifier for this Error. This should only be used in cases of
/// deserialization or testing. If not given, the ID will be auto-generated.
///
/// * `.build()`
/// Finishes the builder and returns a GraphQL [`Error`].
#[builder(visibility = "pub")]
fn new<T: Into<String>>(
fn new(
message: String,
locations: Vec<Location>,
path: Option<Path>,
extension_code: T,
extension_code: Option<String>,
// Skip the `Object` type alias in order to use buildstructor’s map special-casing
mut extensions: JsonMap<ByteString, Value>,
apollo_id: Option<Uuid>,
) -> Self {
extensions
.entry("code")
.or_insert_with(|| extension_code.into().into());
if let Some(code) = extension_code {
extensions
.entry("code")
.or_insert(Value::String(ByteString::from(code)));
}
Self {
message,
locations,
path,
extensions,
apollo_id: apollo_id.unwrap_or_else(Uuid::new_v4),
}
}

Expand Down Expand Up @@ -160,13 +180,25 @@ impl Error {
.map_err(|err| MalformedResponseError {
reason: format!("invalid `path` within error: {}", err),
})?;

Ok(Error {
message,
locations,
path,
extensions,
// TODO confirm camelcase key
let apollo_id: Option<Uuid> = extract_key_value_from_object!(
object,
"apolloId",
Value::String(s) => s
)
.map_err(|err| MalformedResponseError {
reason: format!("invalid `apolloId` within error: {}", err),
})?
.map(|s| {
Uuid::from_str(s.as_str()).map_err(|err| MalformedResponseError {
reason: format!("invalid `apolloId` within error: {}", err),
})
})
.transpose()?;

Ok(Self::new(
message, locations, path, None, extensions, apollo_id,
))
}

pub(crate) fn from_value_completion_value(value: &Value) -> Option<Error> {
Expand Down Expand Up @@ -196,13 +228,41 @@ impl Error {
.and_then(|p: &serde_json_bytes::Value| -> Option<Path> {
serde_json_bytes::from_value(p.clone()).ok()
});
Some(Error {
message,
locations,
path,
extensions,

Some(Self::new(
message, locations, path, None, extensions,
None, // apollo_id is not serialized, so it will never exist in a serialized vc error
))
}

/// Extract the error code from [`Error::extensions`] as a String if it is set.
pub fn extension_code(&self) -> Option<String> {
self.extensions.get("code").and_then(|c| match c {
Value::String(s) => Some(s.as_str().to_owned()),
Value::Bool(b) => Some(format!("{b}")),
Value::Number(n) => Some(n.to_string()),
Value::Null | Value::Array(_) | Value::Object(_) => None,
})
}

/// Retrieve the internal Apollo unique ID for this error
pub fn apollo_id(&self) -> Uuid {
self.apollo_id
}

#[cfg(test)]
/// Null out the ID for comparing errors in tests where you cannot extract the randomly
/// generated Uuid
pub fn with_null_id(mut self) -> Self {
self.apollo_id = Uuid::nil();
self
}
}

/// Generate a random Uuid. For use in generating a default [`Error:apollo_id`] when not supplied
/// during deserialization.
fn generate_uuid() -> Uuid {
Uuid::new_v4()
}

/// GraphQL spec require that both "line" and "column" are positive numbers.
Expand Down Expand Up @@ -282,6 +342,7 @@ impl From<CompilerExecutionError> for Error {
locations,
path,
extensions,
apollo_id: Uuid::new_v4(),
}
}
}
Loading