hive-router 0.2.0

GraphQL router for Federation, part of the Hive platform
use std::sync::Arc;

use hive_console_sdk::circuit_breaker::CircuitBreakerError;
use http::{uri::InvalidUri, HeaderMap, StatusCode};
use rustls::server::VerifierBuilderError;
use strum::IntoStaticStr;

use crate::executor::{
    executors::websocket_client::WsClientError, response::subgraph_response::SubgraphResponse,
};

#[derive(thiserror::Error, Debug, IntoStaticStr)]
pub enum SubgraphExecutorError {
    #[error("Failed to parse endpoint \"{0}\" as URI: {1}")]
    #[strum(serialize = "SUBGRAPH_ENDPOINT_PARSE_FAILURE")]
    EndpointParseFailure(String, InvalidUri),
    #[error("Failed to build WebSocket endpoint \"{0}\" as URI: {1}")]
    #[strum(serialize = "SUBGRAPH_WEBSOCKET_ENDPOINT_BUILD_FAILURE")]
    WebSocketEndpointBuildFailure(String, http::Error),
    #[error("Failed to compile VRL expression for subgraph '{0}'. Please check your VRL expression for syntax errors. Diagnostic: {1}")]
    #[strum(serialize = "SUBGRAPH_ENDPOINT_EXPRESSION_BUILD_FAILURE")]
    EndpointExpressionBuild(String, String),
    #[error("Failed to resolve VRL expression. Runtime error: {0}")]
    #[strum(serialize = "SUBGRAPH_ENDPOINT_EXPRESSION_RESOLUTION_FAILURE")]
    EndpointExpressionResolutionFailure(String),
    #[error("VRL expression resolved to a non-string value.")]
    #[strum(serialize = "SUBGRAPH_ENDPOINT_EXPRESSION_WRONG_TYPE")]
    EndpointExpressionWrongType,
    #[error(
        "Static endpoint not found for subgraph. This is an internal error and should not happen."
    )]
    #[strum(serialize = "SUBGRAPH_STATIC_ENDPOINT_NOT_FOUND")]
    StaticEndpointNotFound,
    #[error("Failed to build request to subgraph: {0}")]
    #[strum(serialize = "SUBGRAPH_REQUEST_BUILD_FAILURE")]
    RequestBuildFailure(#[from] http::Error),
    #[error("Failed to send request to subgraph: {0}")]
    #[strum(serialize = "SUBREQUEST_HTTP_ERROR")]
    RequestFailure(#[from] hyper_util::client::legacy::Error),
    #[error("Failed to receive response: {0}")]
    #[strum(serialize = "SUBGRAPH_RESPONSE_FAILURE")]
    ResponseFailure(#[from] hyper::Error),
    #[error("Failed to serialize variable \"{0}\": {1}")]
    #[strum(serialize = "SUBGRAPH_VARIABLES_SERIALIZATION_FAILURE")]
    VariablesSerializationFailure(String, sonic_rs::Error),
    #[error("Failed to compile VRL expression for timeout for subgraph '{0}'. Please check your VRL expression for syntax errors. Diagnostic: {0}")]
    #[strum(serialize = "SUBGRAPH_TIMEOUT_EXPRESSION_BUILD_FAILURE")]
    RequestTimeoutExpressionBuild(String, String),
    #[error("Failed to resolve VRL expression for timeout for subgraph. Runtime error: {0}")]
    #[strum(serialize = "SUBGRAPH_TIMEOUT_EXPRESSION_RESOLUTION_FAILURE")]
    TimeoutExpressionResolution(String),
    #[error("Request to subgraph timed out")]
    #[strum(serialize = "SUBGRAPH_REQUEST_TIMEOUT")]
    RequestTimeout(#[from] tokio::time::error::Elapsed),
    #[error("Failed to read response body from subgraph \"{0}\": {1}")]
    #[strum(serialize = "SUBGRAPH_RESPONSE_BODY_READ_FAILURE")]
    ResponseBodyReadFailure(String, String, Arc<HeaderMap>),
    #[error("Received empty response body from subgraph \"{0}\"")]
    #[strum(serialize = "SUBGRAPH_RESPONSE_BODY_EMPTY")]
    EmptyResponseBody(String, Arc<HeaderMap>),
    #[error("Failed to deserialize subgraph response: {0}")]
    #[strum(serialize = "SUBREQUEST_MALFORMED_RESPONSE")]
    ResponseDeserializationFailure(sonic_rs::Error, Option<Arc<HeaderMap>>),
    #[error("Subgraph returned malformed response")]
    #[strum(serialize = "SUBREQUEST_MALFORMED_RESPONSE")]
    MalformedResponse(Option<Arc<HeaderMap>>),
    #[error("Invalid content type returned from subgraph '{0}': '{1}'")]
    #[strum(serialize = "SUBREQUEST_MALFORMED_RESPONSE")]
    InvalidContentType(String, String, Arc<HeaderMap>),
    #[error(transparent)]
    #[strum(serialize = "SUBGRAPH_HTTPS_CERTS_FAILURE")]
    TlsCertificatesError(#[from] TlsCertificatesError),
    #[error("Unable to create circuit breaker: {0} for subgraph \"{1}\"")]
    #[strum(serialize = "SUBGRAPH_CIRCUIT_BREAKER_CREATION_FAILURE")]
    CircuitBreakerCreationError(CircuitBreakerError, String),
    #[error("Rejected by the circuit breaker")]
    #[strum(serialize = "SUBGRAPH_CIRCUIT_BREAKER_REJECTED")]
    CircuitBreakerRejected,
    #[error("Unsupported content-type '{0}': expected 'multipart/mixed' or 'text/event-stream' for HTTP subscriptions")]
    #[strum(serialize = "SUBGRAPH_SUBSCRIPTION_UNSUPPORTED_CONTENT_TYPE")]
    UnsupportedContentTypeError(String),
    #[error("Failed to connect WebSocket to '{0}': {1}")]
    #[strum(serialize = "SUBGRAPH_WEBSOCKET_CONNECT_FAILURE")]
    WebSocketConnectFailure(String, String),
    #[error("WebSocket protocol handshake with '{0}' failed: {1}")]
    #[strum(serialize = "SUBGRAPH_WEBSOCKET_HANDSHAKE_FAILURE")]
    WebSocketHandshakeFailure(String, String),
    #[error("WebSocket subscription stream at '{0}' closed before sending any response")]
    #[strum(serialize = "SUBGRAPH_WEBSOCKET_STREAM_CLOSED_EMPTY")]
    WebSocketStreamClosedEmpty(String),
    #[error("WebSocket executor arbiter channel closed unexpectedly")]
    #[strum(serialize = "SUBGRAPH_WEBSOCKET_ARBITER_CHANNEL_CLOSED")]
    WebSocketArbiterChannelClosed,
    #[error("WebSocket client operation failed: {0}")]
    #[strum(serialize = "SUBGRAPH_WEBSOCKET_CLIENT_FAILURE")]
    WebSocketClientFailure(#[from] WsClientError),
    #[error("Failed to parse multipart boundary from Content-Type header: {0}")]
    #[strum(serialize = "SUBGRAPH_SUBSCRIPTION_MULTIPART_BOUNDARY_PARSE_FAILURE")]
    MultipartBoundaryParseFailure(String),
    #[error("Error reading multipart subscription stream: {0}")]
    #[strum(serialize = "SUBGRAPH_SUBSCRIPTION_MULTIPART_STREAM_ERROR")]
    MultipartStreamError(String),
    #[error("Error reading SSE subscription stream: {0}")]
    #[strum(serialize = "SUBGRAPH_SUBSCRIPTION_SSE_STREAM_ERROR")]
    SseStreamError(String),
    #[error("Subgraph stream responded with a not-OK status code '{0}'")]
    #[strum(serialize = "SUBGRAPH_STREAM_STATUS_CODE_NOT_OK")]
    StreamStatusCodeNotOk(StatusCode),
    #[error("Subgraph HTTP callback responded with a not-OK status code '{0}'")]
    #[strum(serialize = "SUBGRAPH_HTTP_CALLBACK_STATUS_CODE_NOT_OK")]
    HttpCallbackStatusCodeNotOk(StatusCode),
    #[error("HTTP Callback protocol does not support single-shot execution, use it only for subscriptions")]
    #[strum(serialize = "SUBGRAPH_HTTP_CALLBACK_NO_SINGLE")]
    HttpCallbackNoSingle,
    #[error("HTTP Callback protocol configured for subgraph but no callback configuration provided for router")]
    #[strum(serialize = "SUBGRAPH_HTTP_CALLBACK_NOT_CONFIGURED")]
    HttpCallbackNotConfigured,
    #[error("Subgraph internal server error")]
    #[strum(serialize = "SUBGRAPH_INTERNAL_SERVER_ERROR")]
    InternalServerError(Box<SubgraphResponse<'static>>),
    #[error(
        "Skipped subgraph execution because the estimated cost exceeds the maximum allowed cost"
    )]
    #[strum(serialize = "SUBGRAPH_COST_ESTIMATED_TOO_EXPENSIVE")]
    CostEstimatedTooExpensive,
}

impl SubgraphExecutorError {
    pub fn error_code(&self) -> &'static str {
        self.into()
    }

    pub fn response_headers(&self) -> Option<&http::HeaderMap> {
        match self {
            Self::InternalServerError(response) => response.headers.as_deref(),
            Self::EmptyResponseBody(_, headers) => Some(headers.as_ref()),
            Self::ResponseBodyReadFailure(_, _, headers) => Some(headers.as_ref()),
            Self::ResponseDeserializationFailure(_, headers) => headers.as_deref(),
            Self::MalformedResponse(headers) => headers.as_deref(),
            Self::InvalidContentType(_, _, headers) => Some(headers.as_ref()),
            _ => None,
        }
    }
}

#[derive(thiserror::Error, Debug, IntoStaticStr)]
pub enum TlsCertificatesError {
    #[error("Failed to initialize or load native TLS root certificates: {0}")]
    NativeTlsCertificatesError(std::io::Error),
    #[error("Failed to load custom TLS certificate {0}: {1}")]
    CustomTlsCertificatesError(&'static str, rustls::pki_types::pem::Error),
    #[error("Unexpected invalid certificates: {0}")]
    InvalidTlsCertificates(String),
    #[error("Failed to build TLS client configuration: {0}")]
    TlsConfigFailure(#[from] rustls::Error),
    #[error("Failed to build TLS verifier: {0}")]
    TlsVerifierFailure(#[from] VerifierBuilderError),
}