1use crate::{
2 handle::ByteResponse,
3 state::{Config, FederatedSchema, State},
4};
5use anyhow::anyhow;
6use apollo_compiler::{
7 ExecutableDocument, Name, Node, Schema,
8 ast::OperationType,
9 executable::{Field, Selection, SelectionSet},
10 request::coerce_variable_values,
11 response::JsonMap,
12 schema::ExtendedType,
13 validation::{Valid, WithErrors},
14};
15use cached::proc_macro::cached;
16use http_body_util::{BodyExt, Empty, Full};
17use hyper::{
18 HeaderMap, Response, StatusCode,
19 body::Bytes,
20 header::{HeaderName, HeaderValue},
21};
22use ordered_float::OrderedFloat;
23use rand::{Rng, rngs::ThreadRng, seq::IteratorRandom};
24use serde::{Deserialize, Deserializer, Serialize};
25use serde_json_bytes::{
26 ByteString, Map, Value, json,
27 serde_json::{self, Number},
28};
29use std::{
30 collections::{BTreeMap, HashMap, HashSet},
31 hash::{DefaultHasher, Hash, Hasher},
32 mem,
33 ops::RangeInclusive,
34 sync::Arc,
35};
36use tracing::{debug, error, trace};
37
38pub async fn handle(
39 body_bytes: Vec<u8>,
40 subgraph_name: Option<&str>,
41 state: Arc<State>,
42) -> anyhow::Result<ByteResponse> {
43 let req: GraphQLRequest = match serde_json::from_slice(&body_bytes) {
44 Ok(req) => req,
45 Err(err) => {
46 error!(%err, "received invalid graphql request");
47 let mut resp = Response::new(
48 Full::new(err.to_string().into_bytes().into())
49 .map_err(|never| match never {})
50 .boxed(),
51 );
52 *resp.status_mut() = StatusCode::BAD_REQUEST;
53
54 return Ok(resp);
55 }
56 };
57
58 let config = state.config.read().await;
59 let schema = state.schema.read().await;
60 let rgen_cfg = subgraph_name
61 .and_then(|name| config.subgraph_overrides.response_generation.get(name))
62 .unwrap_or_else(|| &config.response_generation);
63
64 let mut hasher = DefaultHasher::new();
69 req.query.hash(&mut hasher);
70 rgen_cfg.hash(&mut hasher);
71 schema.hash(&mut hasher);
72 let cache_hash = hasher.finish();
73
74 if let Some((numerator, denominator)) = rgen_cfg.http_error_ratio {
75 let mut rng = rand::rng();
76 if rng.random_ratio(numerator, denominator) {
77 return Response::builder()
78 .status(rng.random_range(500..=504))
79 .body(Empty::new().map_err(|never| match never {}).boxed())
80 .map_err(|err| err.into());
81 }
82 }
83
84 let (bytes, status_code) = if subgraph_name
85 .and_then(|name| config.subgraph_overrides.cache_responses.get(name).copied())
86 .unwrap_or_else(|| config.cache_responses)
87 {
88 into_response_bytes_and_status_code(rgen_cfg, req, &schema, cache_hash).await
89 } else {
90 into_response_bytes_and_status_code_no_cache(rgen_cfg, req, &schema, cache_hash).await
91 };
92
93 let mut resp = Response::new(Full::new(bytes).map_err(|never| match never {}).boxed());
94 *resp.status_mut() = status_code;
95
96 let headers = resp.headers_mut();
97 add_headers(&config, rgen_cfg, subgraph_name, headers);
98
99 Ok(resp)
100}
101
102#[derive(Debug, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct GraphQLRequest {
105 pub query: String,
106 pub operation_name: Option<String>,
107 #[serde(default)]
108 #[serde(deserialize_with = "null_or_missing_as_default")]
109 pub variables: JsonMap,
110}
111
112fn null_or_missing_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
115where
116 D: Deserializer<'de>,
117 T: Default + Deserialize<'de>,
118{
119 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
120}
121
122fn add_headers(
123 config: &Config,
124 rgen_cfg: &ResponseGenerationConfig,
125 subgraph_name: Option<&str>,
126 headers: &mut HeaderMap,
127) {
128 let mut rng = rand::rng();
129
130 let mut last_header_name: HeaderName = HeaderName::from_static("unused");
135 let mut last_ratio: Option<Ratio> = None;
136
137 for (header_name, header_value) in subgraph_name
138 .and_then(|name| config.subgraph_overrides.headers.get(name).cloned())
139 .unwrap_or_else(|| config.headers.clone())
140 .into_iter()
141 {
142 if let Some(name) = header_name {
143 last_ratio = rgen_cfg.header_ratio.get(name.as_str()).copied();
144 last_header_name = name;
145 }
146
147 let should_insert = last_ratio
148 .is_none_or(|(numerator, denominator)| rng.random_ratio(numerator, denominator));
149
150 if should_insert {
151 headers.insert(&last_header_name, header_value);
152 }
153 }
154
155 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
156}
157
158#[cached(result = true, key = "u64", convert = "{_cache_hash}")]
159fn parse_and_validate(
160 req: &GraphQLRequest,
161 schema: &Valid<Schema>,
162 _cache_hash: u64,
163) -> Result<Valid<ExecutableDocument>, WithErrors<ExecutableDocument>> {
164 let op_name = req.operation_name.as_deref().unwrap_or("unknown");
165
166 ExecutableDocument::parse_and_validate(schema, &req.query, op_name)
167}
168
169#[tracing::instrument(skip(req))]
170#[cached(key = "u64", convert = "{cache_hash}")]
171async fn into_response_bytes_and_status_code(
172 cfg: &ResponseGenerationConfig,
173 req: GraphQLRequest,
174 schema: &FederatedSchema,
175 cache_hash: u64,
176) -> (Bytes, StatusCode) {
177 debug!(%cache_hash, "handling graphql request");
178 trace!(variables=?req.variables, "request variables");
179
180 let doc = match parse_and_validate(&req, schema, cache_hash) {
181 Ok(doc) => doc,
182 Err(err) => {
183 let errs: Vec<_> = err.errors.iter().map(|d| d.to_json()).collect();
184 error!(?errs, query=%req.query, "invalid graphql query");
185 let bytes = serde_json::to_vec(&json!({ "data": Value::Null, "errors": errs }))
186 .unwrap_or_default();
187 return (bytes.into(), StatusCode::BAD_REQUEST);
188 }
189 };
190
191 let op = doc.operations.iter().next().unwrap();
192 let op_name = op.name.as_ref().map(|name| name.as_str());
193
194 debug!(
195 ?op_name,
196 type=%op.operation_type,
197 n_selections = op.selection_set.selections.len(),
198 "processing operation"
199 );
200
201 let resp = match op.operation_type {
202 OperationType::Query => {
203 match generate_response(cfg, op_name, &doc, schema, &req.variables) {
204 Ok(resp) => resp,
205 Err(err) => {
206 error!(%err, "unable to generate response");
207 return (
208 Bytes::from("unable to generate response"),
209 StatusCode::INTERNAL_SERVER_ERROR,
210 );
211 }
212 }
213 }
214
215 op_type => {
217 error!("received {op_type} request: not implemented");
218 return (
219 Bytes::from("not implemented"),
220 StatusCode::INTERNAL_SERVER_ERROR,
221 );
222 }
223 };
224
225 match serde_json::to_vec(&resp) {
226 Ok(bytes) => (bytes.into(), StatusCode::OK),
227 Err(err) => {
228 error!(%err, "unable to serialize response");
229 (
230 Bytes::from(err.to_string().into_bytes()),
231 StatusCode::INTERNAL_SERVER_ERROR,
232 )
233 }
234 }
235}
236
237fn generate_response(
238 cfg: &ResponseGenerationConfig,
239 op_name: Option<&str>,
240 doc: &Valid<ExecutableDocument>,
241 schema: &FederatedSchema,
242 variables: &JsonMap,
243) -> anyhow::Result<Value> {
244 let op = match doc.operations.get(op_name) {
245 Ok(op) => op,
246 Err(_) => return Ok(json!({ "data": null })),
247 };
248 let mut rng = rand::rng();
249
250 if let Some((numerator, denominator)) = cfg.graphql_errors.request_error_ratio
251 && rng.random_ratio(numerator, denominator)
252 {
253 return Ok(json!({ "data": null, "errors": [{ "message": "Request error simulated" }]}));
254 }
255
256 if op.is_introspection(doc) {
262 return apollo_compiler::introspection::partial_execute(
263 schema,
264 &schema.implementers_map(),
265 doc,
266 op,
267 &coerce_variable_values(schema, op, variables)
268 .map_err(|err| anyhow!("{}", err.message()))?,
269 )
270 .map_err(|err| anyhow!("{}", err.message()))
271 .and_then(|result| serde_json_bytes::to_value(result).map_err(|err| anyhow!("{}", err)));
272 }
273
274 let mut data =
275 ResponseBuilder::new(&mut rng, doc, schema, cfg).selection_set(&op.selection_set)?;
276
277 if let Some((numerator, denominator)) = cfg.graphql_errors.field_error_ratio
280 && rng.random_ratio(numerator, denominator)
281 {
282 let drop_count = rng.random_range(1..=data.len());
283 let sampled_keys = data.keys().cloned().choose_multiple(&mut rng, drop_count);
284 let to_drop: HashSet<ByteString> = HashSet::from_iter(sampled_keys);
285
286 data.retain(|key, _| !to_drop.contains(key));
287
288 let errors: Vec<Value> = to_drop
289 .into_iter()
290 .map(|key| {
291 json!({
292 "message": "Field error simulated",
293 "path": [key]
294 })
295 })
296 .collect();
297
298 Ok(json!({
299 "data": data,
300 "errors": errors,
301 }))
302 } else {
303 Ok(json!({ "data": data }))
304 }
305}
306
307pub type Ratio = (u32, u32);
308
309#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
310pub struct GraphQLErrorConfig {
311 pub request_error_ratio: Option<Ratio>,
315 pub field_error_ratio: Option<Ratio>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
327pub struct ResponseGenerationConfig {
328 #[serde(default = "default_scalar_config")]
329 pub scalars: BTreeMap<String, ScalarGenerator>,
330 #[serde(default = "default_array_size")]
331 pub array: ArraySize,
332 #[serde(default = "default_null_ratio")]
333 pub null_ratio: Option<Ratio>,
334 #[serde(default)]
335 pub header_ratio: BTreeMap<String, (u32, u32)>,
336 #[serde(default)]
337 pub http_error_ratio: Option<Ratio>,
338 #[serde(default)]
339 pub graphql_errors: GraphQLErrorConfig,
340}
341
342impl ResponseGenerationConfig {
343 pub(crate) fn merge_default_scalars(&mut self) {
346 let default = default_scalar_config();
347 let provided = mem::replace(&mut self.scalars, default);
348 self.scalars.extend(provided);
349 }
350}
351
352impl Default for ResponseGenerationConfig {
353 fn default() -> Self {
354 Self {
355 scalars: default_scalar_config(),
356 array: default_array_size(),
357 null_ratio: default_null_ratio(),
358 header_ratio: BTreeMap::new(),
359 graphql_errors: GraphQLErrorConfig::default(),
360 http_error_ratio: None,
361 }
362 }
363}
364
365fn default_scalar_config() -> BTreeMap<String, ScalarGenerator> {
366 [
367 ("Boolean".into(), ScalarGenerator::Bool),
368 ("Int".into(), ScalarGenerator::Int { min: 0, max: 100 }),
369 ("ID".into(), ScalarGenerator::Int { min: 0, max: 100 }),
370 (
371 "Float".into(),
372 ScalarGenerator::Float {
373 min: OrderedFloat(-1.0),
374 max: OrderedFloat(1.0),
375 },
376 ),
377 (
378 "String".into(),
379 ScalarGenerator::String {
380 min_len: 1,
381 max_len: 10,
382 },
383 ),
384 ]
385 .into_iter()
386 .collect()
387}
388
389fn default_array_size() -> ArraySize {
390 ArraySize {
391 min_length: 0,
392 max_length: 10,
393 }
394}
395
396fn default_null_ratio() -> Option<Ratio> {
397 Some((1, 2))
398}
399
400#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash)]
401#[serde(tag = "type", rename_all = "lowercase")]
402pub enum ScalarGenerator {
403 Bool,
404 Float {
405 min: OrderedFloat<f64>,
406 max: OrderedFloat<f64>,
407 },
408 Int {
409 min: i32,
410 max: i32,
411 },
412 String {
413 min_len: usize,
414 max_len: usize,
415 },
416}
417
418impl Default for ScalarGenerator {
419 fn default() -> Self {
420 Self::DEFAULT
421 }
422}
423
424impl ScalarGenerator {
425 const DEFAULT: Self = Self::String {
426 min_len: 1,
427 max_len: 10,
428 };
429
430 fn generate(&self, rng: &mut ThreadRng) -> anyhow::Result<Value> {
431 let val = match *self {
432 Self::Bool => Value::Bool(rng.random_bool(0.5)),
433 Self::Int { min, max } => Value::Number(rng.random_range(min..=max).into()),
434
435 Self::Float { min, max } => Value::Number(
436 Number::from_f64(rng.random_range(*min..=*max)).expect("expected finite float"),
437 ),
438
439 Self::String { min_len, max_len } => {
442 let len = rng.random_range(min_len..=max_len);
443 let mut chars = Vec::with_capacity(len * 2);
445 for _ in 0..len {
446 chars.push(rng.random::<char>());
447 }
448
449 Value::String(ByteString::from(chars.into_iter().collect::<String>()))
450 }
451 };
452
453 Ok(val)
454 }
455}
456
457#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash)]
458pub struct ArraySize {
459 pub min_length: usize,
460 pub max_length: usize,
461}
462
463impl ArraySize {
464 fn range(&self) -> RangeInclusive<usize> {
465 self.min_length..=self.max_length
466 }
467}
468
469struct ResponseBuilder<'a, 'doc, 'schema> {
470 rng: &'a mut ThreadRng,
471 doc: &'doc Valid<ExecutableDocument>,
472 schema: &'schema FederatedSchema,
473 cfg: &'a ResponseGenerationConfig,
474}
475
476impl<'a, 'doc, 'schema> ResponseBuilder<'a, 'doc, 'schema> {
477 fn new(
478 rng: &'a mut ThreadRng,
479 doc: &'doc Valid<ExecutableDocument>,
480 schema: &'schema FederatedSchema,
481 cfg: &'a ResponseGenerationConfig,
482 ) -> Self {
483 Self {
484 rng,
485 doc,
486 schema,
487 cfg,
488 }
489 }
490
491 fn selection_set(
492 &mut self,
493 selection_set: &SelectionSet,
494 ) -> anyhow::Result<Map<ByteString, Value>> {
495 let grouped_fields = self.collect_fields(selection_set)?;
496 let mut result = Map::new();
497
498 for (key, fields) in grouped_fields {
499 let meta_field = fields[0];
501
502 let val = if meta_field.name == "__typename" {
503 Value::String(ByteString::from(selection_set.ty.to_string()))
504 } else if meta_field.name == "_service" {
505 let mut service_obj = Map::new();
506 service_obj.insert("sdl".to_string(), Value::String(self.schema.sdl().into()));
507 Value::Object(service_obj)
508 } else if !meta_field.ty().is_non_null() && self.should_be_null() {
509 Value::Null
510 } else {
511 let is_selection_set = !meta_field.selection_set.is_empty();
512 let is_array = meta_field.ty().is_list();
513
514 if is_selection_set {
515 let mut selections = Vec::new();
516 for field in fields {
517 selections.extend_from_slice(&field.selection_set.selections);
518 }
519 let full_selection_set = SelectionSet {
520 ty: meta_field.selection_set.ty.clone(),
521 selections,
522 };
523
524 if is_array {
525 Value::Array(self.array_selection_set(&full_selection_set)?)
526 } else {
527 Value::Object(self.selection_set(&full_selection_set)?)
528 }
529 } else {
530 match is_array {
531 false => self.leaf_field(meta_field.ty().inner_named_type())?,
532 true => self.array_leaf_field(meta_field.ty().inner_named_type())?,
533 }
534 }
535 };
536
537 result.insert(key, val);
538 }
539
540 Ok(result)
541 }
542
543 fn collect_fields(
544 &self,
545 selection_set: &'doc SelectionSet,
546 ) -> anyhow::Result<HashMap<String, Vec<&'doc Node<Field>>>> {
547 let mut collected_fields: HashMap<String, Vec<&Node<Field>>> = HashMap::new();
548
549 for selection in &selection_set.selections {
550 match selection {
551 Selection::Field(field) => {
552 let key = field.alias.as_ref().unwrap_or(&field.name).to_string();
553 collected_fields.entry(key).or_default().push(field);
554 }
555 Selection::FragmentSpread(fragment) => {
556 if let Some(fragment_def) = self.doc.fragments.get(&fragment.fragment_name) {
557 for (key, mut fields) in self.collect_fields(&fragment_def.selection_set)? {
558 collected_fields.entry(key).or_default().append(&mut fields);
559 }
560 }
561 }
562 Selection::InlineFragment(inline_fragment) => {
563 for (key, mut fields) in self.collect_fields(&inline_fragment.selection_set)? {
564 collected_fields.entry(key).or_default().append(&mut fields);
565 }
566 }
567 }
568 }
569
570 Ok(collected_fields)
571 }
572
573 fn leaf_field(&mut self, type_name: &Name) -> anyhow::Result<Value> {
574 match self.schema.types.get(type_name).unwrap() {
575 ExtendedType::Enum(enum_ty) => {
576 let enum_value = enum_ty
577 .values
578 .values()
579 .choose(self.rng)
580 .ok_or(anyhow!("empty enum: {type_name}"))?;
581
582 Ok(Value::String(ByteString::from(
583 enum_value.value.to_string(),
584 )))
585 }
586
587 ExtendedType::Scalar(scalar) => self
588 .cfg
589 .scalars
590 .get(scalar.name.as_str())
591 .unwrap_or(&ScalarGenerator::DEFAULT)
592 .generate(self.rng),
593
594 _ => unreachable!("A field with an empty selection set must be a scalar or enum type"),
595 }
596 }
597
598 fn arbitrary_array_len(&mut self) -> anyhow::Result<usize> {
599 Ok(self.rng.random_range(self.cfg.array.range()))
600 }
601
602 fn array_selection_set(&mut self, selection_set: &SelectionSet) -> anyhow::Result<Vec<Value>> {
603 let num_values = self.arbitrary_array_len()?;
604 let mut values = Vec::with_capacity(num_values);
605 for _ in 0..num_values {
606 values.push(Value::Object(self.selection_set(selection_set)?));
607 }
608
609 Ok(values)
610 }
611
612 fn array_leaf_field(&mut self, type_name: &Name) -> anyhow::Result<Value> {
613 let num_values = self.arbitrary_array_len()?;
614 let mut values = Vec::with_capacity(num_values);
615 for _ in 0..num_values {
616 values.push(self.leaf_field(type_name)?);
617 }
618
619 Ok(Value::Array(values))
620 }
621
622 fn should_be_null(&mut self) -> bool {
623 if let Some((numerator, denominator)) = self.cfg.null_ratio {
624 self.rng.random_ratio(numerator, denominator)
625 } else {
626 false
627 }
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn introspection_short_circuits() -> anyhow::Result<()> {
637 let supergraph = include_str!("../../tests/data/schema.graphql");
638 let schema = FederatedSchema::parse_string(supergraph, "../../tests/data/schema.graphql")?;
639
640 let query = r#"
641 query {
642 __schema {
643 queryType {
644 name
645 }
646 types {
647 name
648 kind
649 }
650 }
651 }
652 "#;
653
654 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
655 let cfg = ResponseGenerationConfig::default();
656 let result = generate_response(&cfg, None, &doc, &schema, &JsonMap::new())?;
657
658 assert!(result.get("data").is_some());
659 let data = result.get("data").unwrap();
660 assert!(data.get("__schema").is_some());
661 assert!(data.as_object().unwrap().len() == 1);
663
664 let schema_obj = data.get("__schema").unwrap();
665 assert!(schema_obj.get("queryType").is_some());
666
667 let query_type = schema_obj.get("queryType").unwrap();
668 assert_eq!(query_type.get("name").unwrap().as_str().unwrap(), "Query");
669
670 let types = schema_obj.get("types").unwrap().as_array().unwrap();
671 assert!(!types.is_empty());
672
673 let type_names: Vec<&str> = types
674 .iter()
675 .filter_map(|t| t.get("name")?.as_str())
676 .collect();
677 assert!(type_names.contains(&"Query"));
678 assert!(type_names.contains(&"User"));
679 assert!(type_names.contains(&"Post"));
680
681 Ok(())
682 }
683
684 #[test]
685 fn service_introspection_uses_raw_schema() -> anyhow::Result<()> {
686 let supergraph = include_str!("../../tests/data/schema.graphql");
687 let schema = FederatedSchema::parse_string(supergraph, "../../tests/data/schema.graphql")?;
688
689 let query = r#"
690 query {
691 _service {
692 sdl
693 }
694 }
695 "#;
696
697 let doc = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap();
698 let cfg = ResponseGenerationConfig::default();
699 let result = generate_response(&cfg, None, &doc, &schema, &JsonMap::new())?;
700
701 assert!(result.get("data").is_some());
702 let data = result.get("data").unwrap();
703 assert!(data.get("_service").is_some());
704
705 let schema_obj = data.get("_service").unwrap();
706 assert!(schema_obj.get("sdl").is_some());
707
708 let sdl = schema_obj.get("sdl").unwrap().as_str().unwrap();
709 assert_eq!(supergraph, sdl);
710
711 Ok(())
712 }
713}