use std::convert::Infallible;
use std::sync::Arc;
use std::task::{Context, Poll};
use async_graphql::http::is_accept_multipart_mixed;
use async_graphql::parser::types::OperationType;
use async_graphql::{BatchRequest, Executor, Request as GraphQLInnerRequest, ServerError};
use async_graphql_axum::rejection::GraphQLRejection;
use async_graphql_axum::{GraphQLBatchRequest, GraphQLRequest, GraphQLResponse};
use axum::BoxError;
use axum::body::{Body, HttpBody};
use axum::extract::FromRequest;
use axum::http::{Request as HttpRequest, Response as HttpResponse, StatusCode};
use axum::response::IntoResponse;
use bytes::Bytes;
use futures_util::future::BoxFuture;
use http::header::{CONTENT_TYPE, HeaderValue};
use surrealdb_core::dbs::Session;
use surrealdb_core::dbs::capabilities::RouteTarget;
use surrealdb_core::gql::cache::GraphQLSchemaCache;
use surrealdb_core::observe::Outcome;
use tower_service::Service;
use web_time::Instant;
use crate::ntw::error::Error as NetError;
fn graphql_operation_type_label(req: &mut GraphQLInnerRequest) -> &'static str {
let operation_name = req.operation_name.clone();
let Ok(doc) = req.parsed_query() else {
return "unknown";
};
let op = match operation_name.as_deref() {
Some(selected) => doc.operations.iter().find_map(|(name, op)| {
if name.is_some_and(|n| n.as_str() == selected) {
Some(op.node.ty)
} else {
None
}
}),
None => doc.operations.iter().next().map(|(_, op)| op.node.ty),
};
match op {
Some(OperationType::Query) => "query",
Some(OperationType::Mutation) => "mutation",
Some(OperationType::Subscription) => "subscription",
None => "unknown",
}
}
#[derive(Clone)]
pub struct GraphQLService {
cache: GraphQLSchemaCache,
}
impl GraphQLService {
pub fn new() -> Self {
GraphQLService {
cache: GraphQLSchemaCache::default(),
}
}
pub(crate) fn cache(&self) -> GraphQLSchemaCache {
self.cache.clone()
}
}
impl<B> Service<HttpRequest<B>> for GraphQLService
where
B: HttpBody<Data = Bytes> + Send + 'static,
B::Data: Into<Bytes>,
B::Error: Into<BoxError>,
{
type Response = HttpResponse<Body>;
type Error = Infallible;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: HttpRequest<B>) -> Self::Future {
let cache = self.cache.clone();
let req = req.map(Body::new);
Box::pin(async move {
let state = req
.extensions()
.get::<crate::ntw::AppState>()
.expect("state extractor should always succeed");
let datastore = &state.datastore;
let metrics_observer = state.metrics_observer.clone();
let started_at = Instant::now();
if !datastore.allows_http_route(&RouteTarget::GraphQL) {
warn!(
"Capabilities denied HTTP route request attempt, target: '{}'",
&RouteTarget::GraphQL
);
return Ok(
NetError::ForbiddenRoute(RouteTarget::GraphQL.to_string()).into_response()
);
}
let session =
req.extensions().get::<Session>().expect("session extractor should always succeed");
let Some(_ns) = session.ns.as_ref() else {
return Ok(graphql_error_response(
"No namespace specified. Set the `surreal-ns` header on the request.",
));
};
let Some(_db) = session.db.as_ref() else {
return Ok(graphql_error_response(
"No database specified. Set the `surreal-db` header on the request.",
));
};
let schema = match cache.get_schema(datastore, session).await {
Ok(e) => e,
Err(e) => {
info!(?e, "error generating schema");
return Ok(graphql_error_response(&format!("{e}")));
}
};
let datastore_ctx = Arc::clone(datastore);
let session_ctx = std::sync::Arc::new(session.clone());
let is_accept_multipart_mixed = req
.headers()
.get("accept")
.and_then(|value| value.to_str().ok())
.map(is_accept_multipart_mixed)
.unwrap_or_default();
let metric_ns = session_ctx.ns.clone();
let metric_db = session_ctx.db.clone();
let metric_user = if session_ctx.au.is_anon() {
None
} else if session_ctx.au.is_record() {
Some("<record>".to_owned())
} else {
Some(session_ctx.au.id().to_owned())
};
if is_accept_multipart_mixed {
let gql_req = match GraphQLRequest::<GraphQLRejection>::from_request(req, &()).await
{
Ok(r) => r,
Err(err) => return Ok(err.into_response()),
};
let mut req_with_data = gql_req.into_inner().data(datastore_ctx).data(session_ctx);
let op_label = graphql_operation_type_label(&mut req_with_data);
if request_is_subscription(&mut req_with_data) {
let response = async_graphql::Response::from_errors(vec![ServerError::new(
"Subscriptions require WebSocket transport on GET /graphql",
None,
)]);
Ok(as_application_json(GraphQLResponse::from(response).into_response()))
} else {
let response = schema.execute(req_with_data).await;
if let Some(observer) = metrics_observer.as_ref() {
let outcome = if response.is_err() {
Outcome::Error
} else {
Outcome::Success
};
let error_class = if response.is_err() {
Some(surrealdb_core::observe::error_class::CLIENT)
} else {
None
};
observer.record_graphql_operation(
op_label,
outcome,
error_class,
started_at.elapsed(),
metric_ns.as_deref(),
metric_db.as_deref(),
metric_user.as_deref(),
);
}
Ok(as_application_json(GraphQLResponse::from(response).into_response()))
}
} else {
let gql_req =
match GraphQLBatchRequest::<GraphQLRejection>::from_request(req, &()).await {
Ok(r) => r,
Err(err) => return Ok(err.into_response()),
};
let mut batch_req = gql_req.into_inner();
let op_label = match &mut batch_req {
BatchRequest::Single(req) => graphql_operation_type_label(req),
BatchRequest::Batch(_) => "batch",
};
let req_with_data = batch_req.data(datastore_ctx).data(session_ctx);
let response = schema.execute_batch(req_with_data).await;
if let Some(observer) = metrics_observer.as_ref() {
let any_err = !response.is_ok();
let outcome = if any_err {
Outcome::Error
} else {
Outcome::Success
};
let error_class = if any_err {
Some(surrealdb_core::observe::error_class::CLIENT)
} else {
None
};
observer.record_graphql_operation(
op_label,
outcome,
error_class,
started_at.elapsed(),
metric_ns.as_deref(),
metric_db.as_deref(),
metric_user.as_deref(),
);
}
Ok(as_application_json(GraphQLResponse(response).into_response()))
}
})
}
}
fn request_is_subscription(req: &mut GraphQLInnerRequest) -> bool {
let operation_name = req.operation_name.clone();
let Ok(doc) = req.parsed_query() else {
return false;
};
match operation_name.as_deref() {
Some(selected) => doc.operations.iter().any(|(name, op)| {
name.is_some_and(|n| n.as_str() == selected)
&& matches!(op.node.ty, OperationType::Subscription)
}),
None => doc
.operations
.iter()
.next()
.is_some_and(|(_, op)| matches!(op.node.ty, OperationType::Subscription)),
}
}
fn as_application_json(mut response: HttpResponse<Body>) -> HttpResponse<Body> {
response.headers_mut().insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
response
}
fn graphql_error_response(message: &str) -> HttpResponse<Body> {
let body = serde_json::json!({
"data": null,
"errors": [{ "message": message }],
})
.to_string();
let mut response = HttpResponse::new(Body::from(body));
*response.status_mut() = StatusCode::BAD_REQUEST;
response.headers_mut().insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
response
}