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