use std::{sync::Arc, vec};
use graphql_tools::validation::utils::ValidationError;
use hive_router_plan_executor::{
execution::{error::PlanExecutionError, jwt_forward::JwtForwardingError},
headers::errors::HeaderRuleRuntimeError,
hooks::on_graphql_error::handle_graphql_errors_with_plugins,
response::graphql_error::GraphQLError,
};
use hive_router_query_planner::{
ast::normalization::error::NormalizationError, planner::PlannerError,
};
use http::{header::RETRY_AFTER, HeaderName, Method, StatusCode};
use ntex::{
http::ResponseBuilder,
web::{self, error::QueryPayloadError},
};
use serde::Serialize;
use strum::IntoStaticStr;
use crate::{
jwt::errors::JwtError,
pipeline::{
authorization::AuthorizationError, body_read::ReadBodyStreamError, header::ResponseMode,
progressive_override::LabelEvaluationError,
},
RouterSharedState,
};
#[derive(Debug, thiserror::Error, IntoStaticStr)]
pub enum PipelineError {
#[error("Unsupported HTTP method: {0}")]
#[strum(serialize = "METHOD_NOT_ALLOWED")]
UnsupportedHttpMethod(Method),
#[error("Header '{0}' has invalid value")]
#[strum(serialize = "INVALID_HEADER")]
InvalidHeaderValue(HeaderName),
#[error("Content-Type header is missing")]
#[strum(serialize = "MISSING_CONTENT_TYPE_HEADER")]
MissingContentTypeHeader,
#[error("Content-Type header is not supported")]
#[strum(serialize = "UNSUPPORTED_CONTENT_TYPE")]
UnsupportedContentType,
#[error("Failed to deserialize query parameters")]
#[strum(serialize = "INVALID_QUERY_PARAMS")]
GetInvalidQueryParams,
#[error("Missing query parameter: {0}")]
#[strum(serialize = "MISSING_QUERY_PARAM")]
GetMissingQueryParam(&'static str),
#[error("Cannot perform mutations over GET")]
#[strum(serialize = "MUTATION_NOT_ALLOWED_OVER_HTTP_GET")]
MutationNotAllowedOverHttpGet,
#[error("Failed to parse query parameters")]
#[strum(serialize = "UNPROCESSABLE_QUERY_PARAMS")]
GetUnprocessableQueryParams(#[from] QueryPayloadError),
#[error("Failed to parse GraphQL request payload")]
#[strum(serialize = "BAD_REQUEST")]
FailedToParseBody(sonic_rs::Error),
#[error("Failed to parse GraphQL variables JSON")]
#[strum(serialize = "BAD_REQUEST")]
FailedToParseVariables(sonic_rs::Error),
#[error("Failed to parse GraphQL extensions JSON")]
#[strum(serialize = "BAD_REQUEST")]
FailedToParseExtensions(sonic_rs::Error),
#[error("Failed to parse GraphQL operation: {0}")]
#[strum(serialize = "GRAPHQL_PARSE_FAILED")]
FailedToParseOperation(#[from] Arc<graphql_tools::parser::query::ParseError>),
#[error("Failed to minify parsed GraphQL operation: {0}")]
#[strum(serialize = "GRAPHQL_PARSE_MINIFY_FAILED")]
FailedToMinifyParsedOperation(String),
#[error("Failed to normalize GraphQL operation")]
#[strum(serialize = "OPERATION_RESOLUTION_FAILURE")]
NormalizationError(#[from] Arc<NormalizationError>),
#[error("Failed to collect GraphQL variables: {0}")]
#[strum(serialize = "BAD_USER_INPUT")]
VariablesCoercionError(String),
#[error("Validation errors")]
#[strum(serialize = "GRAPHQL_VALIDATION_FAILED")]
ValidationErrors(Arc<Vec<ValidationError>>),
#[error("Authorization failed")]
#[strum(serialize = "UNAUTHORIZED_OPERATION")]
AuthorizationFailed(Vec<AuthorizationError>),
#[error("Failed to execute a plan: {0}")]
#[strum(serialize = "PLAN_EXECUTION_FAILED")]
PlanExecutionError(#[from] PlanExecutionError),
#[error("Failed to produce a plan: {0}")]
#[strum(serialize = "QUERY_PLAN_BUILD_FAILED")]
PlannerError(#[from] Arc<PlannerError>),
#[error(transparent)]
#[strum(serialize = "OVERRIDE_LABEL_EVALUATION_FAILED")]
LabelEvaluationError(#[from] LabelEvaluationError),
#[error("Required CSRF header(s) not present")]
#[strum(serialize = "CSRF_PREVENTION_FAILED")]
CsrfPreventionFailed,
#[error(transparent)]
#[strum(serialize = "JWT_ERROR")]
JwtError(#[from] JwtError),
#[error("Failed to forward jwt: {0}")]
#[strum(serialize = "JWT_FORWARDING_ERROR")]
JwtForwardingError(#[from] JwtForwardingError),
#[error("Failed to evaluate introspection expression: {0}")]
#[strum(serialize = "INTROSPECTION_PERMISSION_EVALUATION_ERROR")]
IntrospectionPermissionEvaluationError(String),
#[error("Introspection queries are disabled")]
#[strum(serialize = "INTROSPECTION_DISABLED")]
IntrospectionDisabled,
#[error("Subscriptions are not supported")]
#[strum(serialize = "SUBSCRIPTIONS_NOT_SUPPORTED")]
SubscriptionsNotSupported,
#[error("Subscriptions are not supported over accepted transport(s)")]
#[strum(serialize = "SUBSCRIPTIONS_TRANSPORT_NOT_SUPPORTED")]
SubscriptionsTransportNotSupported,
#[error(transparent)]
#[strum(serialize = "READ_BODY_STREAM_ERROR")]
ReadBodyStreamError(#[from] ReadBodyStreamError),
#[error("Request timed out")]
#[strum(serialize = "GATEWAY_TIMEOUT")]
TimeoutError,
#[error(transparent)]
#[strum(serialize = "HEADER_PROPAGATION_FAILURE")]
HeaderPropagation(#[from] HeaderRuleRuntimeError),
#[error("Failed to serialize the query plan: {0}")]
#[strum(serialize = "QUERY_PLAN_SERIALIZATION_FAILED")]
QueryPlanSerializationFailed(sonic_rs::Error),
#[error("No supergraph available yet, unable to process request")]
#[strum(serialize = "NO_SUPERGRAPH_AVAILABLE")]
NoSupergraphAvailable,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum ParserCacheError {
#[error("Failed to parse GraphQL operation: {0}")]
ParseError(Arc<graphql_tools::parser::query::ParseError>),
#[error("Failed to minify parsed GraphQL operation: {0}")]
MinifyError(String),
#[error("Validation errors")]
ValidationErrors(Arc<Vec<ValidationError>>),
}
impl From<Arc<ParserCacheError>> for PipelineError {
fn from(value: Arc<ParserCacheError>) -> Self {
match value.as_ref() {
ParserCacheError::ParseError(err) => PipelineError::FailedToParseOperation(err.clone()),
ParserCacheError::MinifyError(err) => {
PipelineError::FailedToMinifyParsedOperation(err.clone())
}
ParserCacheError::ValidationErrors(errs) => {
PipelineError::ValidationErrors(errs.clone())
}
}
}
}
impl PipelineError {
pub fn graphql_error_code(&self) -> &'static str {
match self {
Self::JwtError(err) => err.error_code(),
Self::PlanExecutionError(err) => err.error_code(),
Self::ReadBodyStreamError(err) => err.error_code(),
_ => self.into(),
}
}
pub fn graphql_error_message(&self) -> String {
match self {
Self::PlannerError(_) => "Unexpected error".to_string(),
_ => self.to_string(),
}
}
pub fn default_status_code(&self, prefer_ok: bool) -> StatusCode {
match (self, prefer_ok) {
(Self::PlannerError(_), _) => StatusCode::INTERNAL_SERVER_ERROR,
(Self::PlanExecutionError(_), _) => StatusCode::INTERNAL_SERVER_ERROR,
(Self::LabelEvaluationError(_), _) => StatusCode::INTERNAL_SERVER_ERROR,
(Self::JwtForwardingError(_), _) => StatusCode::INTERNAL_SERVER_ERROR,
(Self::UnsupportedHttpMethod(_), _) => StatusCode::METHOD_NOT_ALLOWED,
(Self::InvalidHeaderValue(_), _) => StatusCode::BAD_REQUEST,
(Self::GetUnprocessableQueryParams(_), _) => StatusCode::BAD_REQUEST,
(Self::GetInvalidQueryParams, _) => StatusCode::BAD_REQUEST,
(Self::GetMissingQueryParam(_), _) => StatusCode::BAD_REQUEST,
(Self::FailedToParseBody(_), _) => StatusCode::BAD_REQUEST,
(Self::FailedToParseVariables(_), _) => StatusCode::BAD_REQUEST,
(Self::FailedToParseExtensions(_), _) => StatusCode::BAD_REQUEST,
(Self::FailedToParseOperation(_), false) => StatusCode::BAD_REQUEST,
(Self::FailedToParseOperation(_), true) => StatusCode::OK,
(Self::FailedToMinifyParsedOperation(_), false) => StatusCode::BAD_REQUEST,
(Self::FailedToMinifyParsedOperation(_), true) => StatusCode::OK,
(Self::NormalizationError(_), _) => StatusCode::BAD_REQUEST,
(Self::VariablesCoercionError(_), false) => StatusCode::BAD_REQUEST,
(Self::VariablesCoercionError(_), true) => StatusCode::OK,
(Self::MutationNotAllowedOverHttpGet, _) => StatusCode::METHOD_NOT_ALLOWED,
(Self::ValidationErrors(_), true) => StatusCode::OK,
(Self::ValidationErrors(_), false) => StatusCode::BAD_REQUEST,
(Self::AuthorizationFailed(_), _) => StatusCode::FORBIDDEN,
(Self::MissingContentTypeHeader, _) => StatusCode::NOT_ACCEPTABLE,
(Self::UnsupportedContentType, _) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
(Self::CsrfPreventionFailed, _) => StatusCode::FORBIDDEN,
(Self::JwtError(err), _) => err.status_code(),
(Self::IntrospectionPermissionEvaluationError(_), _) => {
StatusCode::INTERNAL_SERVER_ERROR
}
(Self::IntrospectionDisabled, _) => StatusCode::FORBIDDEN,
(Self::SubscriptionsNotSupported, _) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
(Self::SubscriptionsTransportNotSupported, _) => StatusCode::NOT_ACCEPTABLE,
(Self::ReadBodyStreamError(err), _) => err.status_code(),
(Self::TimeoutError, _) => StatusCode::GATEWAY_TIMEOUT,
(Self::HeaderPropagation(_), _) => StatusCode::INTERNAL_SERVER_ERROR,
(Self::QueryPlanSerializationFailed(_), _) => StatusCode::INTERNAL_SERVER_ERROR,
(Self::NoSupergraphAvailable, _) => StatusCode::SERVICE_UNAVAILABLE,
}
}
}
#[derive(Serialize)]
struct FailedExecutionResult {
errors: Vec<GraphQLError>,
}
#[inline]
pub fn handle_pipeline_error(
err: PipelineError,
shared_state: &RouterSharedState,
response_mode: &ResponseMode,
) -> web::HttpResponse {
let single_content_type = response_mode.single_content_type();
let prefer_ok = response_mode.prefer_status_ok_for_errors();
let status = err.default_status_code(prefer_ok);
let mut res = ResponseBuilder::new(status);
if let Some(single_content_type) = single_content_type {
res.content_type(single_content_type.as_ref());
}
if matches!(err, PipelineError::NoSupergraphAvailable) {
res.header(RETRY_AFTER, "10");
}
let mut errors = match err {
PipelineError::ValidationErrors(validation_errors) => {
validation_errors.iter().map(|error| error.into()).collect()
}
PipelineError::AuthorizationFailed(authorization_errors) => authorization_errors
.iter()
.map(|error| error.into())
.collect(),
_ => {
let code = err.graphql_error_code();
let message = err.graphql_error_message();
let graphql_error = GraphQLError::from_message_and_code(message, code);
vec![graphql_error]
}
};
if let Some(plugins) = &shared_state.plugins {
let (new_errors, new_status_code) =
handle_graphql_errors_with_plugins(plugins, errors, status);
errors = new_errors;
res.status(new_status_code);
}
if let Some(error_recorder) = shared_state
.telemetry_context
.metrics
.graphql
.error_recorder()
{
error_recorder
.record_errors(|| errors.iter().map(|error| error.extensions.code.as_deref()));
}
res.json(&FailedExecutionResult { errors })
}