apollo_router/
introspection.rs1use std::num::NonZeroUsize;
2use std::ops::ControlFlow;
3use std::sync::Arc;
4
5use apollo_compiler::executable::Selection;
6use serde_json_bytes::json;
7use sha2::Digest;
8use sha2::Sha256;
9
10use crate::Configuration;
11use crate::cache::storage::CacheStorage;
12use crate::compute_job;
13use crate::compute_job::ComputeBackPressureError;
14use crate::compute_job::ComputeJobType;
15use crate::graphql;
16use crate::json_ext::Object;
17use crate::query_planner::QueryKey;
18use crate::services::layers::query_analysis::ParsedDocument;
19use crate::spec;
20
21const DEFAULT_INTROSPECTION_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(5).unwrap();
22
23#[derive(Clone)]
24pub(crate) struct IntrospectionCache(Mode);
25
26#[derive(Clone)]
27enum Mode {
28 Disabled,
29 Enabled {
30 storage: Arc<CacheStorage<String, graphql::Response>>,
31 max_depth: MaxDepth,
32 },
33}
34
35#[derive(Copy, Clone)]
36enum MaxDepth {
37 Check,
38 Ignore,
39}
40
41impl IntrospectionCache {
42 pub(crate) fn new(configuration: &Configuration) -> Self {
43 if configuration.supergraph.introspection {
44 let storage = Arc::new(CacheStorage::new_in_memory(
45 DEFAULT_INTROSPECTION_CACHE_CAPACITY,
46 "introspection",
47 ));
48 storage.activate();
49 Self(Mode::Enabled {
50 storage,
51 max_depth: if configuration.limits.introspection_max_depth {
52 MaxDepth::Check
53 } else {
54 MaxDepth::Ignore
55 },
56 })
57 } else {
58 Self(Mode::Disabled)
59 }
60 }
61
62 pub(crate) fn activate(&self) {
63 match &self.0 {
64 Mode::Disabled => {}
65 Mode::Enabled { storage, .. } => storage.activate(),
66 }
67 }
68
69 pub(crate) async fn maybe_execute(
72 &self,
73 schema: &Arc<spec::Schema>,
74 key: &QueryKey,
75 doc: &ParsedDocument,
76 variables: Object,
77 ) -> ControlFlow<Result<graphql::Response, ComputeBackPressureError>, ()> {
78 Self::maybe_lone_root_typename(schema, doc)?;
79 if doc.operation.is_query() {
80 if doc.has_schema_introspection {
81 if doc.has_explicit_root_fields {
82 ControlFlow::Break(Ok(Self::mixed_fields_error()))?;
83 } else {
84 ControlFlow::Break(
85 self.cached_introspection(schema, key, doc, variables).await,
86 )?
87 }
88 } else if !doc.has_explicit_root_fields {
89 let max_depth = MaxDepth::Ignore;
93
94 ControlFlow::Break(Ok(Self::execute_introspection(
96 max_depth, schema, doc, variables,
97 )))?
98 }
99 }
100 ControlFlow::Continue(())
101 }
102
103 fn maybe_lone_root_typename(
109 schema: &Arc<spec::Schema>,
110 doc: &ParsedDocument,
111 ) -> ControlFlow<Result<graphql::Response, ComputeBackPressureError>, ()> {
112 if doc.operation.selection_set.selections.len() == 1
113 && let Selection::Field(field) = &doc.operation.selection_set.selections[0]
114 && field.name == "__typename"
115 && field.directives.is_empty()
116 {
117 let key = field.response_key().as_str();
120 let object_type_name = schema
121 .api_schema()
122 .root_operation(doc.operation.operation_type)
123 .expect("validation should have caught undefined root operation")
124 .as_str();
125 let data = json!({key: object_type_name});
126 ControlFlow::Break(Ok(graphql::Response::builder().data(data).build()))?
127 }
128 ControlFlow::Continue(())
129 }
130
131 fn mixed_fields_error() -> graphql::Response {
132 let error = graphql::Error::builder()
133 .message(
134 "\
135 Mixed queries with both schema introspection and concrete fields \
136 are not supported yet: https://github.com/apollographql/router/issues/2789\
137 ",
138 )
139 .extension_code("MIXED_INTROSPECTION")
140 .build();
141 graphql::Response::builder().error(error).build()
142 }
143
144 fn introspection_cache_key(query: &str, variables: Object) -> Option<String> {
145 if let Ok(variable_key) = serde_json::to_string(&variables) {
146 let mut hasher = Sha256::new();
147 hasher.update(variable_key);
148 Some(format!("{query}:{:x}", hasher.finalize()))
149 } else {
150 tracing::warn!(
151 "Failed to serialize variables for introspection cache key, skipping cache: {:?}",
152 variables
153 );
154 None
155 }
156 }
157
158 async fn cached_introspection(
159 &self,
160 schema: &Arc<spec::Schema>,
161 key: &QueryKey,
162 doc: &ParsedDocument,
163 variables: Object,
164 ) -> Result<graphql::Response, ComputeBackPressureError> {
165 let (storage, max_depth) = match &self.0 {
166 Mode::Enabled { storage, max_depth } => (storage, *max_depth),
167 Mode::Disabled => {
168 let error = graphql::Error::builder()
169 .message(String::from("introspection has been disabled"))
170 .extension_code("INTROSPECTION_DISABLED")
171 .build();
172 return Ok(graphql::Response::builder().error(error).build());
173 }
174 };
175
176 let cache_key = Self::introspection_cache_key(&key.filtered_query, variables.clone());
177 if let Some(cache_key) = &cache_key
178 && let Some(response) = storage.get(cache_key, |_| unreachable!()).await
179 {
180 return Ok(response);
181 }
182
183 let schema = schema.clone();
184 let doc = doc.clone();
185 let response = compute_job::execute(ComputeJobType::Introspection, move |_| {
186 Self::execute_introspection(max_depth, &schema, &doc, variables)
187 })?
188 .await;
194
195 if let Some(cache_key) = cache_key {
196 storage.insert(cache_key, response.clone()).await;
197 }
198 Ok(response)
199 }
200
201 fn execute_introspection(
202 max_depth: MaxDepth,
203 schema: &spec::Schema,
204 doc: &ParsedDocument,
205 variables: Object,
206 ) -> graphql::Response {
207 let api_schema = schema.api_schema();
208 let operation = &doc.operation;
209 let max_depth_result = match max_depth {
210 MaxDepth::Check => {
211 apollo_compiler::introspection::check_max_depth(&doc.executable, operation)
212 }
213 MaxDepth::Ignore => Ok(()),
214 };
215 let result = max_depth_result
216 .and_then(|()| {
217 apollo_compiler::request::coerce_variable_values(api_schema, operation, &variables)
218 })
219 .and_then(|variable_values| {
220 apollo_compiler::introspection::partial_execute(
221 api_schema,
222 &schema.implementers_map,
223 &doc.executable,
224 operation,
225 &variable_values,
226 )
227 });
228 match result {
229 Ok(response) => response.into(),
230 Err(e) => {
231 let error = e.to_graphql_error(&doc.executable.sources);
232 graphql::Response::builder().error(error).build()
233 }
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use serde_json_bytes::json;
241
242 use crate::introspection::IntrospectionCache;
243
244 #[test]
245 fn test_variable_normalization_key() {
246 let variables = json!({
247 "e": true,
248 "a": "John Doe",
249 "b": 30,
250 "f": null,
251 "d": {
252 "b": 1,
253 "a": 2,
254 },
255 "c": [1, "Hello", { "d": "World","a": 3 }],
256 });
257 let key = IntrospectionCache::introspection_cache_key(
258 "query { __typename }",
259 variables.as_object().unwrap().clone(),
260 )
261 .unwrap();
262 assert_eq!(
263 key,
264 "query { __typename }:618d257f0ab1069a2374274c8c6e56f6b6528a839045647cedcfd147bc5dd9cf"
265 );
266 }
267}