Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f35cb08
No preserve order, serialize the response using selection set in order
ardatan Jun 25, 2025
3e82aa4
Representations
ardatan Jun 26, 2025
572649b
Escape strings
ardatan Jun 26, 2025
08e70ad
FF
ardatan Jun 26, 2025
e6a2db9
Fix
ardatan Jun 27, 2025
01fc14a
Go
ardatan Jun 27, 2025
6bec030
Rebase
ardatan Jun 27, 2025
89691bd
Refactor projection to use a string builder (#209)
kamilkisiela Jun 27, 2025
b81b2cf
Refactor traverse_and_collect to use a recursive helper (#208)
kamilkisiela Jun 27, 2025
46af0ab
Improve `project_requires` method (#210)
kamilkisiela Jun 27, 2025
353aa85
Allocation-free JSON string escaping (#212)
kamilkisiela Jun 27, 2025
0b36ca1
Use HashSet for possible types and inline
kamilkisiela Jun 27, 2025
8b6daf6
Fix clippy
ardatan Jun 27, 2025
de01474
Fix options
ardatan Jun 27, 2025
09c7c55
Fix obj empty case (#213)
ardatan Jun 27, 2025
3535f30
Use callbacks to avoid vector allocations and avoid temporary allocat…
ardatan Jun 30, 2025
61acc98
Format
ardatan Jun 30, 2025
09f02de
Fix null-keys on perf improvements (#217)
ardatan Jun 30, 2025
5de27ef
Add missing fragment spread
ardatan Jul 2, 2025
56df831
Fix provides-on-union on the new serialization impl (#218)
ardatan Jul 8, 2025
9e04921
Fix requires-with-argument / handle variables correctly on serializat…
ardatan Jul 8, 2025
9dec09d
Fix simple-interface-object audit / fix possible type check (#224)
ardatan Jul 8, 2025
b782fba
Fixes based on main
ardatan Jul 8, 2025
dd786e5
Fixes based on main
ardatan Jul 9, 2025
45eb8e1
Improvements
ardatan Jul 9, 2025
d79baea
Avoid .remove
ardatan Jul 9, 2025
0d71c01
No reqwest (#254)
ardatan Jul 16, 2025
f8e45f1
Fix progressive override
ardatan Jul 16, 2025
3d1d005
Remove extra string endpoint property in HTTPSubgraphExecutor
ardatan Jul 16, 2025
c6afa58
Rebase from main
ardatan Jul 18, 2025
5d525e4
Fix format and clippy errors
ardatan Jul 18, 2025
757a21c
Projection preparation to handle conflicting fields (#285)
ardatan Jul 21, 2025
fd835e7
Remove flamegraph
ardatan Jul 21, 2025
7189b3d
Remove HttpRequestParams layer to prevent extra copy/clone/allocation…
ardatan Jul 21, 2025
6702d9b
Use io::Write (#288)
ardatan Jul 23, 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
776 changes: 170 additions & 606 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions bench/k6.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { githubComment } from "https://raw.githubusercontent.com/dotansimha/k6-g

const endpoint = __ENV.GATEWAY_ENDPOINT || "http://0.0.0.0:4000/graphql";
const vus = __ENV.BENCH_VUS ? parseInt(__ENV.BENCH_VUS) : 50;
const time = __ENV.BENCH_OVER_TIME || "30s";
const duration = __ENV.BENCH_OVER_TIME || "30s";

export const options = {
vus: vus,
duration: time,
vus,
duration,
};

export function setup() {
Expand Down Expand Up @@ -57,7 +57,7 @@ export function handleSummary(data) {
},
});
}
return handleBenchmarkSummary(data, { vus, time });
return handleBenchmarkSummary(data, { vus, duration });
}

let printIdentifiersMap = {};
Expand Down
2 changes: 1 addition & 1 deletion bin/dev-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ query-planner = { path = "../../lib/query-planner" }
graphql-parser = "0.4.1"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing-tree = "0.4.0"
serde_json = { version = "1.0.120", features = ["preserve_order"] }
serde_json = "1.0.140"
6 changes: 5 additions & 1 deletion bin/gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ path = "src/main.rs"
query-planner = { path = "../../lib/query-planner" }
query-plan-executor = { path = "../../lib/query-plan-executor" }

mimalloc = { version = "0.1.47", features = ["override"] }

# Tokio runtime
tokio = { version = "1.38.0", features = ["full"] }

Expand All @@ -20,7 +22,8 @@ graphql-tools = "0.4.0" # Using version from original file

# Serialization
serde = { version = "1.0.203", features = ["derive"] }
serde_json = { version = "1.0.120", features = ["preserve_order"] }
serde_json = "1.0.140"
sonic-rs = "0.3"

# HTTP client and caching
moka = { version = "0.12.8", features = ["future"] }
Expand Down Expand Up @@ -48,6 +51,7 @@ tower-http = { version = "0.6.6", features = [
"cors",
"request-id",
] }
http-body-util = "0.1.3"

# Utils
thiserror = "2.0.12"
Expand Down
8 changes: 5 additions & 3 deletions bin/gateway/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use axum::{
Router,
};
use http::Request;
use mimalloc::MiMalloc;
use tokio::signal;

use axum::Extension;
Expand All @@ -28,7 +29,6 @@ use crate::pipeline::{
coerce_variables_service::CoerceVariablesService, execution_service::ExecutionService,
graphiql_service::GraphiQLResponderService,
graphql_request_params::GraphQLRequestParamsExtractor,
http_request_params::HttpRequestParamsExtractor,
normalize_service::GraphQLOperationNormalizationService, parser_service::GraphQLParserService,
progressive_override_service::ProgressiveOverrideExtractor,
query_plan_service::QueryPlanService, validation_service::GraphQLValidationService,
Expand All @@ -43,6 +43,9 @@ use tower_http::{
};
use tracing::info;

#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let perfetto_file = env::var("PERFETTO_OUT").ok().is_some_and(|v| v == "1");
Expand Down Expand Up @@ -95,14 +98,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
)
}),
)
.layer(HttpRequestParamsExtractor::new_layer())
.layer(GraphiQLResponderService::new_layer())
.layer(GraphQLRequestParamsExtractor::new_layer())
.layer(GraphQLParserService::new_layer())
.layer(GraphQLValidationService::new_layer())
.layer(ProgressiveOverrideExtractor::new_layer())
.layer(GraphQLOperationNormalizationService::new_layer())
.layer(CoerceVariablesService::new_layer())
.layer(GraphQLValidationService::new_layer())
.layer(QueryPlanService::new_layer())
.layer(PropagateRequestIdLayer::new(REQUEST_ID_HEADER_NAME.clone()))
.service(ExecutionService::new(expose_query_plan));
Expand Down
54 changes: 33 additions & 21 deletions bin/gateway/src/pipeline/coerce_variables_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ use std::collections::HashMap;
use std::sync::Arc;

use axum::body::Body;
use http::Request;
use http::{Method, Request};
use query_plan_executor::variables::collect_variables;
use query_plan_executor::ExecutionRequest;
use query_planner::state::supergraph_state::OperationKind;
use serde_json::Value;
use tracing::{trace, warn};
use tracing::{error, trace, warn};

use crate::pipeline::error::{PipelineError, PipelineErrorVariant};
use crate::pipeline::error::{PipelineError, PipelineErrorFromAcceptHeader, PipelineErrorVariant};
use crate::pipeline::gateway_layer::{
GatewayPipelineLayer, GatewayPipelineStepDecision, ProcessorLayer,
};
use crate::pipeline::http_request_params::HttpRequestParams;
use crate::pipeline::graphql_request_params::ExecutionRequest;
use crate::pipeline::normalize_service::GraphQLNormalizationPayload;
use crate::shared_state::GatewaySharedState;

Expand All @@ -35,30 +35,44 @@ impl GatewayPipelineLayer for CoerceVariablesService {
#[tracing::instrument(level = "trace", name = "CoerceVariablesService", skip_all)]
async fn process(
&self,
mut req: Request<Body>,
) -> Result<(Request<Body>, GatewayPipelineStepDecision), PipelineError> {
req: &mut Request<Body>,
) -> Result<GatewayPipelineStepDecision, PipelineError> {
let normalized_operation = req
.extensions()
.get::<GraphQLNormalizationPayload>()
.get::<Arc<GraphQLNormalizationPayload>>()
.ok_or_else(|| {
PipelineErrorVariant::InternalServiceError("GraphQLNormalizationPayload is missing")
req.new_pipeline_error(PipelineErrorVariant::InternalServiceError(
"GraphQLNormalizationPayload is missing",
))
})?;

let http_payload = req.extensions().get::<HttpRequestParams>().ok_or_else(|| {
PipelineErrorVariant::InternalServiceError("HttpRequestParams is missing")
})?;

let execution_params = req.extensions().get::<ExecutionRequest>().ok_or_else(|| {
PipelineErrorVariant::InternalServiceError("ExecutionRequest is missing")
req.new_pipeline_error(PipelineErrorVariant::InternalServiceError(
"ExecutionRequest is missing",
))
})?;

let app_state = req
.extensions()
.get::<Arc<GatewaySharedState>>()
.ok_or_else(|| {
PipelineErrorVariant::InternalServiceError("GatewaySharedState is missing")
req.new_pipeline_error(PipelineErrorVariant::InternalServiceError(
"GatewaySharedState is missing",
))
})?;

if req.method() == Method::GET {
if let Some(OperationKind::Mutation) =
normalized_operation.operation_for_plan.operation_kind
{
error!("Mutation is not allowed over GET, stopping");

return Err(
req.new_pipeline_error(PipelineErrorVariant::MutationNotAllowedOverHttpGet)
);
}
}

match collect_variables(
&normalized_operation.operation_for_plan,
&execution_params.variables,
Expand All @@ -74,18 +88,16 @@ impl GatewayPipelineLayer for CoerceVariablesService {
variables_map: values,
});

Ok((req, GatewayPipelineStepDecision::Continue))
Ok(GatewayPipelineStepDecision::Continue)
}
Err(err_msg) => {
warn!(
"failed to collect variables from incoming request: {}",
err_msg
);

return Err(PipelineError::new_with_accept_header(
PipelineErrorVariant::VariablesCoercionError(err_msg),
http_payload.accept_header.clone(),
));
return Err(
req.new_pipeline_error(PipelineErrorVariant::VariablesCoercionError(err_msg))
);
}
}
}
Expand Down
82 changes: 36 additions & 46 deletions bin/gateway/src/pipeline/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@ use std::{collections::HashMap, sync::Arc};

use axum::{body::Body, extract::rejection::QueryRejection, response::IntoResponse};
use graphql_tools::validation::utils::ValidationError;
use http::{Response, StatusCode};
use http::{HeaderName, Method, Request, Response, StatusCode};
use query_plan_executor::{ExecutionResult, GraphQLError};
use query_planner::{ast::normalization::error::NormalizationError, planner::PlannerError};
use serde_json::Value;

use crate::pipeline::http_request_params::APPLICATION_JSON;
use crate::pipeline::header::{RequestAccepts, APPLICATION_GRAPHQL_RESPONSE_JSON_STR};

#[derive(Debug)]
pub struct PipelineError {
pub accept_header: Option<String>,
pub accept_ok: bool,
pub error: PipelineErrorVariant,
}

impl PipelineError {
pub fn new_with_accept_header(
error: PipelineErrorVariant,
accept_header_value: String,
) -> Self {
Self {
accept_header: Some(accept_header_value),
error,
}
pub trait PipelineErrorFromAcceptHeader {
fn new_pipeline_error(&self, error: PipelineErrorVariant) -> PipelineError;
}

impl PipelineErrorFromAcceptHeader for Request<Body> {
fn new_pipeline_error(&self, error: PipelineErrorVariant) -> PipelineError {
let accept_ok = !self.accepts_content_type(&APPLICATION_GRAPHQL_RESPONSE_JSON_STR);
PipelineError { accept_ok, error }
}
}

Expand All @@ -35,9 +34,9 @@ pub enum PipelineErrorVariant {

// HTTP-related errors
#[error("Unsupported HTTP method: {0}")]
UnsupportedHttpMethod(String),
UnsupportedHttpMethod(Method),
#[error("Header '{0}' has invalid value")]
InvalidHeaderValue(String),
InvalidHeaderValue(HeaderName),
#[error("Failed to read body: {0}")]
FailedToReadBodyBytes(axum::Error),
#[error("Content-Type header is missing")]
Expand All @@ -55,11 +54,11 @@ pub enum PipelineErrorVariant {

// GraphQL-specific errors
#[error("Failed to parse GraphQL request payload")]
FailedToParseBody(serde_json::Error),
FailedToParseBody(sonic_rs::Error),
#[error("Failed to parse GraphQL variables JSON")]
FailedToParseVariables(serde_json::Error),
FailedToParseVariables(sonic_rs::Error),
#[error("Failed to parse GraphQL extensions JSON")]
FailedToParseExtensions(serde_json::Error),
FailedToParseExtensions(sonic_rs::Error),
#[error("Failed to parse GraphQL operation")]
FailedToParseOperation(graphql_parser::query::ParseError),
#[error("Failed to normalize GraphQL operation")]
Expand All @@ -78,19 +77,27 @@ impl PipelineErrorVariant {
Self::UnsupportedHttpMethod(_) => "METHOD_NOT_ALLOWED",
Self::PlannerError(_) => "QUERY_PLAN_BUILD_FAILED",
Self::InternalServiceError(_) => "INTERNAL_SERVER_ERROR",
Self::FailedToParseOperation(_) => "GRAPHQL_PARSE_FAILED",
Self::ValidationErrors(_) => "GRAPHQL_VALIDATION_FAILED",
Self::VariablesCoercionError(_) => "BAD_USER_INPUT",
Self::NormalizationError(NormalizationError::OperationNotFound) => {
"OPERATION_RESOLUTION_FAILURE"
}
Self::NormalizationError(NormalizationError::SpecifiedOperationNotFound {
operation_name: _,
}) => "OPERATION_RESOLUTION_FAILURE",
Self::NormalizationError(NormalizationError::MultipleMatchingOperationsFound) => {
"OPERATION_RESOLUTION_FAILURE"
}
_ => "BAD_REQUEST",
}
}

pub fn graphql_error_message(&self) -> String {
match self {
Self::PlannerError(_) | Self::InternalServiceError(_) => {
return "Unexpected error".to_string()
}
_ => {}
Self::PlannerError(_) | Self::InternalServiceError(_) => "Unexpected error".to_string(),
_ => self.to_string(),
}

self.to_string()
}

pub fn default_status_code(&self, prefer_ok: bool) -> StatusCode {
Expand All @@ -111,34 +118,17 @@ impl PipelineErrorVariant {
(Self::VariablesCoercionError(_), false) => StatusCode::BAD_REQUEST,
(Self::VariablesCoercionError(_), true) => StatusCode::OK,
(Self::MutationNotAllowedOverHttpGet, _) => StatusCode::METHOD_NOT_ALLOWED,
(Self::ValidationErrors(_), _) => StatusCode::BAD_REQUEST,
(Self::MissingContentTypeHeader, _) => StatusCode::BAD_REQUEST,
(Self::UnsupportedContentType, _) => StatusCode::BAD_REQUEST,
}
}
}

impl From<PipelineError> for PipelineErrorVariant {
fn from(error: PipelineError) -> Self {
error.error
}
}

impl From<PipelineErrorVariant> for PipelineError {
fn from(error: PipelineErrorVariant) -> Self {
Self {
error,
accept_header: None,
(Self::ValidationErrors(_), true) => StatusCode::OK,
(Self::ValidationErrors(_), false) => StatusCode::BAD_REQUEST,
(Self::MissingContentTypeHeader, _) => StatusCode::NOT_ACCEPTABLE,
(Self::UnsupportedContentType, _) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
}
}
}

impl IntoResponse for PipelineError {
fn into_response(self) -> Response<Body> {
let accept_ok = self
.accept_header
.is_some_and(|v| v.contains(APPLICATION_JSON.to_str().unwrap()));
let status = self.error.default_status_code(accept_ok);
let status = self.error.default_status_code(self.accept_ok);

if let PipelineErrorVariant::ValidationErrors(validation_errors) = self.error {
let validation_error_result = ExecutionResult {
Expand All @@ -148,7 +138,7 @@ impl IntoResponse for PipelineError {
};

return (
StatusCode::OK,
status,
serde_json::to_string(&validation_error_result).unwrap(),
)
.into_response();
Expand Down
Loading
Loading