1#![allow(missing_docs)] use std::collections::HashSet;
4use std::fmt::Display;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::Duration;
8
9use apollo_compiler::validation::Valid;
10use http::StatusCode;
11use http::Version;
12use itertools::Itertools;
13use multimap::MultiMap;
14use serde::Deserialize;
15use serde::Serialize;
16use serde_json_bytes::ByteString;
17use serde_json_bytes::Map as JsonMap;
18use serde_json_bytes::Value;
19use sha2::Digest;
20use sha2::Sha256;
21use static_assertions::assert_impl_all;
22use tokio::sync::broadcast;
23use tokio::sync::mpsc;
24use tokio_stream::Stream;
25use tower::BoxError;
26
27use crate::Context;
28use crate::batching::BatchQuery;
29use crate::error::Error;
30use crate::graphql;
31use crate::http_ext::TryIntoHeaderName;
32use crate::http_ext::TryIntoHeaderValue;
33use crate::http_ext::header_map;
34use crate::json_ext::Object;
35use crate::json_ext::Path;
36use crate::plugins::authentication::APOLLO_AUTHENTICATION_JWT_CLAIMS;
37use crate::plugins::authentication::subgraph::SigningParamsConfig;
38use crate::plugins::authorization::CacheKeyMetadata;
39use crate::plugins::response_cache::cache_control::CacheControl;
40use crate::query_planner::fetch::OperationKind;
41use crate::spec::QueryHash;
42
43pub type BoxService = tower::util::BoxService<Request, Response, BoxError>;
44pub type BoxCloneService = tower::util::BoxCloneService<Request, Response, BoxError>;
45pub type ServiceResult = Result<Response, BoxError>;
46pub(crate) type BoxGqlStream = Pin<Box<dyn Stream<Item = graphql::Response> + Send + Sync>>;
47#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub struct SubgraphRequestId(pub String);
50
51impl Display for SubgraphRequestId {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}", self.0)
54 }
55}
56
57assert_impl_all!(Request: Send);
58#[non_exhaustive]
59pub struct Request {
60 pub supergraph_request: Arc<http::Request<graphql::Request>>,
62
63 pub subgraph_request: http::Request<graphql::Request>,
64
65 pub operation_kind: OperationKind,
66
67 pub context: Context,
68
69 pub(crate) subgraph_name: String,
71 pub(crate) subscription_stream: Option<mpsc::Sender<BoxGqlStream>>,
73 pub(crate) connection_closed_signal: Option<broadcast::Receiver<()>>,
75
76 pub(crate) query_hash: Arc<QueryHash>,
77
78 pub(crate) authorization: Arc<CacheKeyMetadata>,
80
81 pub(crate) executable_document: Option<Arc<Valid<apollo_compiler::ExecutableDocument>>>,
82
83 pub(crate) id: SubgraphRequestId,
85}
86
87#[buildstructor::buildstructor]
88impl Request {
89 #[builder(visibility = "pub")]
93 fn new(
94 supergraph_request: Arc<http::Request<graphql::Request>>,
95 subgraph_request: http::Request<graphql::Request>,
96 operation_kind: OperationKind,
97 context: Context,
98 subscription_stream: Option<mpsc::Sender<BoxGqlStream>>,
99 subgraph_name: String,
100 connection_closed_signal: Option<broadcast::Receiver<()>>,
101 executable_document: Option<Arc<Valid<apollo_compiler::ExecutableDocument>>>,
102 ) -> Request {
103 Self {
104 supergraph_request,
105 subgraph_request,
106 operation_kind,
107 context,
108 subgraph_name,
109 subscription_stream,
110 connection_closed_signal,
111 query_hash: QueryHash::default().into(),
115 authorization: Default::default(),
116 executable_document,
117 id: SubgraphRequestId::new(),
118 }
119 }
120
121 #[builder(visibility = "pub")]
127 fn fake_new(
128 supergraph_request: Option<Arc<http::Request<graphql::Request>>>,
129 subgraph_request: Option<http::Request<graphql::Request>>,
130 operation_kind: Option<OperationKind>,
131 context: Option<Context>,
132 subscription_stream: Option<mpsc::Sender<BoxGqlStream>>,
133 subgraph_name: Option<String>,
134 connection_closed_signal: Option<broadcast::Receiver<()>>,
135 ) -> Request {
136 Request::new(
137 supergraph_request.unwrap_or_default(),
138 subgraph_request.unwrap_or_default(),
139 operation_kind.unwrap_or(OperationKind::Query),
140 context.unwrap_or_default(),
141 subscription_stream,
142 subgraph_name.unwrap_or_default(),
143 connection_closed_signal,
144 None,
145 )
146 }
147
148 pub(crate) fn is_part_of_batch(&self) -> bool {
149 self.context
150 .extensions()
151 .with_lock(|lock| lock.contains_key::<BatchQuery>())
152 }
153
154 pub(crate) fn subgraph_operation_name(&self) -> Option<&str> {
155 self.subgraph_request.body().operation_name.as_deref()
156 }
157
158 pub(crate) fn root_operation_fields(&self) -> Vec<String> {
159 self.executable_document
160 .as_ref()
161 .and_then(|executable_document| {
162 let operation_name = self.subgraph_operation_name();
163 Some(
164 executable_document
165 .operations
166 .get(operation_name)
167 .ok()?
168 .root_fields(executable_document)
169 .map(|f| f.name.to_string())
170 .collect(),
171 )
172 })
173 .unwrap_or_default()
174 }
175}
176
177impl Clone for Request {
178 fn clone(&self) -> Self {
179 let mut builder = http::Request::builder()
181 .method(self.subgraph_request.method())
182 .version(self.subgraph_request.version())
183 .uri(self.subgraph_request.uri());
184
185 {
186 let headers = builder.headers_mut().unwrap();
187 headers.extend(
188 self.subgraph_request
189 .headers()
190 .iter()
191 .map(|(name, value)| (name.clone(), value.clone())),
192 );
193 }
194 let mut subgraph_request = builder.body(self.subgraph_request.body().clone()).unwrap();
195 if let Some(signing_params) = self
203 .subgraph_request
204 .extensions()
205 .get::<Arc<SigningParamsConfig>>()
206 .cloned()
207 {
208 subgraph_request.extensions_mut().insert(signing_params);
209 }
210
211 Self {
212 supergraph_request: self.supergraph_request.clone(),
213 subgraph_request,
214 operation_kind: self.operation_kind,
215 context: self.context.clone(),
216 subgraph_name: self.subgraph_name.clone(),
217 subscription_stream: self.subscription_stream.clone(),
218 connection_closed_signal: self
219 .connection_closed_signal
220 .as_ref()
221 .map(|s| s.resubscribe()),
222 query_hash: self.query_hash.clone(),
223 authorization: self.authorization.clone(),
224 executable_document: self.executable_document.clone(),
225 id: self.id.clone(),
226 }
227 }
228}
229
230impl SubgraphRequestId {
231 pub fn new() -> Self {
232 SubgraphRequestId(
233 uuid::Uuid::new_v4()
234 .as_hyphenated()
235 .encode_lower(&mut uuid::Uuid::encode_buffer())
236 .to_string(),
237 )
238 }
239}
240
241impl std::ops::Deref for SubgraphRequestId {
242 type Target = str;
243
244 fn deref(&self) -> &str {
245 &self.0
246 }
247}
248
249impl Default for SubgraphRequestId {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255assert_impl_all!(Response: Send);
256#[derive(Debug)]
257#[non_exhaustive]
258pub struct Response {
259 pub response: http::Response<graphql::Response>,
260 pub(crate) subgraph_name: String,
262 pub context: Context,
263 pub(crate) id: SubgraphRequestId,
265}
266
267#[buildstructor::buildstructor]
268impl Response {
269 pub(crate) fn new_from_response(
274 response: http::Response<graphql::Response>,
275 context: Context,
276 subgraph_name: String,
277 id: SubgraphRequestId,
278 ) -> Self {
279 Self {
280 response,
281 context,
282 subgraph_name,
283 id,
284 }
285 }
286
287 #[builder(visibility = "pub")]
292 fn new(
293 label: Option<String>,
294 data: Option<Value>,
295 path: Option<Path>,
296 errors: Vec<Error>,
297 extensions: Object,
298 status_code: Option<StatusCode>,
299 context: Context,
300 headers: Option<http::HeaderMap<http::HeaderValue>>,
301 subgraph_name: String,
302 id: Option<SubgraphRequestId>,
303 ) -> Self {
304 let res = graphql::Response::builder()
306 .and_label(label)
307 .data(data.unwrap_or_default())
308 .and_path(path)
309 .errors(errors)
310 .extensions(extensions)
311 .build();
312
313 let mut response = http::Response::builder()
315 .status(status_code.unwrap_or(StatusCode::OK))
316 .body(res)
317 .expect("Response is serializable; qed");
318
319 *response.headers_mut() = headers.unwrap_or_default();
320
321 let id = id.unwrap_or_default();
325
326 Self {
327 response,
328 context,
329 subgraph_name,
330 id,
331 }
332 }
333
334 #[builder(visibility = "pub")]
340 fn fake_new(
341 label: Option<String>,
342 data: Option<Value>,
343 path: Option<Path>,
344 errors: Vec<Error>,
345 extensions: JsonMap<ByteString, Value>,
347 status_code: Option<StatusCode>,
348 context: Option<Context>,
349 headers: Option<http::HeaderMap<http::HeaderValue>>,
350 subgraph_name: Option<String>,
351 id: Option<SubgraphRequestId>,
352 ) -> Self {
353 Self::new(
354 label,
355 data,
356 path,
357 errors,
358 extensions,
359 status_code,
360 context.unwrap_or_default(),
361 headers,
362 subgraph_name.unwrap_or_default(),
363 id,
364 )
365 }
366
367 #[builder(visibility = "pub")]
374 fn fake2_new(
375 label: Option<String>,
376 data: Option<Value>,
377 path: Option<Path>,
378 errors: Vec<Error>,
379 extensions: JsonMap<ByteString, Value>,
381 status_code: Option<StatusCode>,
382 context: Option<Context>,
383 headers: MultiMap<TryIntoHeaderName, TryIntoHeaderValue>,
384 subgraph_name: Option<String>,
385 id: Option<SubgraphRequestId>,
386 ) -> Result<Response, BoxError> {
387 Ok(Self::new(
388 label,
389 data,
390 path,
391 errors,
392 extensions,
393 status_code,
394 context.unwrap_or_default(),
395 Some(header_map(headers)?),
396 subgraph_name.unwrap_or_default(),
397 id,
398 ))
399 }
400
401 #[builder(visibility = "pub")]
405 fn error_new(
406 errors: Vec<Error>,
407 status_code: Option<StatusCode>,
408 context: Context,
409 subgraph_name: String,
410 id: Option<SubgraphRequestId>,
411 ) -> Self {
412 Self::new(
413 Default::default(),
414 Default::default(),
415 Default::default(),
416 errors,
417 Default::default(),
418 status_code,
419 context,
420 Default::default(),
421 subgraph_name,
422 id,
423 )
424 }
425
426 pub(crate) fn subgraph_cache_control(
427 &self,
428 default_ttl: Option<Duration>,
429 ) -> Result<CacheControl, BoxError> {
430 Ok(CacheControl::try_from(self.response.headers())?.with_default_ttl(default_ttl))
431 }
432
433 pub(crate) fn get_from_extensions(&self, key: &str) -> Option<&Value> {
434 self.response.body().extensions.get(key)
435 }
436}
437
438impl Request {
439 pub(crate) fn to_sha256(
440 &self,
441 ignored_headers: &HashSet<String>,
442 ignore_auth_context: bool,
443 ) -> String {
444 let mut hasher = Sha256::new();
445 let http_req = &self.subgraph_request;
446 hasher.update(http_req.method().as_str().as_bytes());
447
448 let version = match http_req.version() {
450 Version::HTTP_09 => "HTTP/0.9",
451 Version::HTTP_10 => "HTTP/1.0",
452 Version::HTTP_11 => "HTTP/1.1",
453 Version::HTTP_2 => "HTTP/2.0",
454 Version::HTTP_3 => "HTTP/3.0",
455 _ => "unknown",
456 };
457 hasher.update(version.as_bytes());
458 let uri = http_req.uri();
459 if let Some(scheme) = uri.scheme() {
460 hasher.update(scheme.as_str().as_bytes());
461 }
462 if let Some(authority) = uri.authority() {
463 hasher.update(authority.as_str().as_bytes());
464 }
465 if let Some(query) = uri.query() {
466 hasher.update(query.as_bytes());
467 }
468
469 let mut headers: Vec<(&[u8], &[u8])> = Vec::with_capacity(http_req.headers().len());
486 headers.extend(
487 http_req
488 .headers()
489 .iter()
490 .filter(|(name, _)| !ignored_headers.contains(name.as_str()))
491 .map(|(name, value)| (name.as_str().as_bytes(), value.as_bytes())),
492 );
493 hasher.update(b"\0H");
494 sort_and_hash(&mut hasher, headers);
495
496 if !ignore_auth_context
497 && let Some(claim) = self
498 .context
499 .get_json_value(APOLLO_AUTHENTICATION_JWT_CLAIMS)
500 {
501 hasher.update(b"\0C");
502 hasher.update(format!("{claim:?}").as_bytes());
503 }
504 let body = http_req.body();
505 if let Some(operation_name) = &body.operation_name {
506 hasher.update(b"\0O");
507 hasher.update(operation_name.as_bytes());
508 }
509 if let Some(query) = &body.query {
510 hasher.update(b"\0Q");
511 hasher.update(query.as_bytes());
512 }
513 hasher.update(b"\0V");
519 sort_and_hash(
520 &mut hasher,
521 body.variables
522 .iter()
523 .map(|(k, v)| (k.inner(), v.to_bytes())),
524 );
525 hasher.update(b"\0E");
526 sort_and_hash(
527 &mut hasher,
528 body.extensions
529 .iter()
530 .map(|(k, v)| (k.inner(), v.to_bytes())),
531 );
532
533 hex::encode(hasher.finalize())
534 }
535}
536
537fn sort_and_hash(
540 hasher: &mut Sha256,
541 pairs: impl IntoIterator<Item = (impl AsRef<[u8]>, impl AsRef<[u8]>)>,
542) {
543 let sorted = pairs
544 .into_iter()
545 .sorted_unstable_by(|a, b| a.0.as_ref().cmp(b.0.as_ref()));
546 for (k, v) in sorted {
547 hasher.update(k.as_ref());
548 hasher.update([0]);
549 hasher.update(v.as_ref());
550 hasher.update([0]);
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_subgraph_request_hash() {
560 let subgraph_req_1 = Request::fake_builder()
561 .subgraph_request(
562 http::Request::builder()
563 .header("public_header", "value")
564 .header("auth", "my_token")
565 .body(graphql::Request::default())
566 .unwrap(),
567 )
568 .build();
569 let subgraph_req_2 = Request::fake_builder()
570 .subgraph_request(
571 http::Request::builder()
572 .header("public_header", "value_bis")
573 .header("auth", "my_token")
574 .body(graphql::Request::default())
575 .unwrap(),
576 )
577 .build();
578 let mut ignored_headers = HashSet::new();
579 ignored_headers.insert("public_header".to_string());
580 assert_eq!(
581 subgraph_req_1.to_sha256(&ignored_headers, false),
582 subgraph_req_2.to_sha256(&ignored_headers, false)
583 );
584
585 let subgraph_req_1 = Request::fake_builder()
586 .subgraph_request(
587 http::Request::builder()
588 .header("public_header", "value")
589 .header("auth", "my_token")
590 .body(graphql::Request::default())
591 .unwrap(),
592 )
593 .build();
594 let subgraph_req_2 = Request::fake_builder()
595 .subgraph_request(
596 http::Request::builder()
597 .header("public_header", "value_bis")
598 .header("auth", "my_token")
599 .body(graphql::Request::default())
600 .unwrap(),
601 )
602 .build();
603 let ignored_headers = HashSet::new();
604 assert_ne!(
605 subgraph_req_1.to_sha256(&ignored_headers, false),
606 subgraph_req_2.to_sha256(&ignored_headers, false)
607 );
608 }
609
610 #[test]
611 fn test_subgraph_request_hash_ignore_auth_context() {
612 use serde_json_bytes::json;
613
614 let req_with_claims_a = Request::fake_builder()
616 .subgraph_request(
617 http::Request::builder()
618 .body(graphql::Request::default())
619 .unwrap(),
620 )
621 .build();
622 req_with_claims_a
623 .context
624 .insert(APOLLO_AUTHENTICATION_JWT_CLAIMS, json!({"sub": "user-a"}))
625 .expect("insert JWT claims");
626
627 let req_with_claims_b = Request::fake_builder()
628 .subgraph_request(
629 http::Request::builder()
630 .body(graphql::Request::default())
631 .unwrap(),
632 )
633 .build();
634 req_with_claims_b
635 .context
636 .insert(APOLLO_AUTHENTICATION_JWT_CLAIMS, json!({"sub": "user-b"}))
637 .expect("insert JWT claims");
638
639 let ignored_headers = HashSet::new();
640
641 assert_ne!(
643 req_with_claims_a.to_sha256(&ignored_headers, false),
644 req_with_claims_b.to_sha256(&ignored_headers, false),
645 "requests with different JWT claims must hash differently by default"
646 );
647
648 assert_eq!(
650 req_with_claims_a.to_sha256(&ignored_headers, true),
651 req_with_claims_b.to_sha256(&ignored_headers, true),
652 "requests with different JWT claims must hash identically when ignore_auth_context is true"
653 );
654 }
655
656 #[test]
657 fn test_clone_does_not_copy_arbitrary_subgraph_request_extensions() {
658 #[derive(Clone, PartialEq, Debug)]
664 struct ShouldNotSurviveClone(u32);
665
666 let mut req = Request::fake_builder()
667 .subgraph_request(
668 http::Request::builder()
669 .body(graphql::Request::default())
670 .unwrap(),
671 )
672 .build();
673 req.subgraph_request
674 .extensions_mut()
675 .insert(ShouldNotSurviveClone(42));
676
677 let cloned = req.clone();
678 assert!(
679 cloned
680 .subgraph_request
681 .extensions()
682 .get::<ShouldNotSurviveClone>()
683 .is_none(),
684 "arbitrary extension types must not be copied when SubgraphRequest is cloned"
685 );
686 }
687
688 #[test]
689 fn test_subgraph_request_hash_no_delimiter_collision() {
690 let req_two_headers = Request::fake_builder()
694 .subgraph_request(
695 http::Request::builder()
696 .header("x", "y")
697 .header("xy", "")
698 .body(graphql::Request::default())
699 .unwrap(),
700 )
701 .build();
702 let req_one_header = Request::fake_builder()
703 .subgraph_request(
704 http::Request::builder()
705 .header("x", "yxy")
706 .body(graphql::Request::default())
707 .unwrap(),
708 )
709 .build();
710 let ignored_headers = HashSet::new();
711 assert_ne!(
712 req_two_headers.to_sha256(&ignored_headers, false),
713 req_one_header.to_sha256(&ignored_headers, false),
714 "header pairs must be delimited so concatenations cannot collide"
715 );
716 }
717
718 #[test]
719 fn test_subgraph_request_hash_non_ascii_value_distinguishable() {
720 let req_a = Request::fake_builder()
724 .subgraph_request(
725 http::Request::builder()
726 .header(
727 "x-custom",
728 http::HeaderValue::from_bytes(&[0xC3, 0xA9]).unwrap(),
729 )
730 .body(graphql::Request::default())
731 .unwrap(),
732 )
733 .build();
734 let req_b = Request::fake_builder()
735 .subgraph_request(
736 http::Request::builder()
737 .header(
738 "x-custom",
739 http::HeaderValue::from_bytes(&[0xC3, 0xB1]).unwrap(),
740 )
741 .body(graphql::Request::default())
742 .unwrap(),
743 )
744 .build();
745 let ignored_headers = HashSet::new();
746 assert_ne!(
747 req_a.to_sha256(&ignored_headers, false),
748 req_b.to_sha256(&ignored_headers, false),
749 "non-ASCII header values must not be collapsed to a single sentinel"
750 );
751 }
752
753 #[test]
754 fn test_subgraph_request_hash_variables_order_independence() {
755 use serde_json_bytes::json;
756
757 let mut vars_a = JsonMap::new();
758 vars_a.insert("a", json!(1));
759 vars_a.insert("b", json!(2));
760 vars_a.insert("c", json!(3));
761 let mut vars_b = JsonMap::new();
762 vars_b.insert("c", json!(3));
763 vars_b.insert("a", json!(1));
764 vars_b.insert("b", json!(2));
765
766 let req_a = Request::fake_builder()
767 .subgraph_request(
768 http::Request::builder()
769 .body(graphql::Request::builder().variables(vars_a).build())
770 .unwrap(),
771 )
772 .build();
773 let req_b = Request::fake_builder()
774 .subgraph_request(
775 http::Request::builder()
776 .body(graphql::Request::builder().variables(vars_b).build())
777 .unwrap(),
778 )
779 .build();
780 let ignored_headers = HashSet::new();
781 assert_eq!(
782 req_a.to_sha256(&ignored_headers, false),
783 req_b.to_sha256(&ignored_headers, false),
784 "two requests with the same variables in different insertion orders must hash identically"
785 );
786 }
787
788 #[test]
789 fn test_subgraph_request_hash_variables_no_delimiter_collision() {
790 use serde_json_bytes::json;
791
792 let mut vars_two = JsonMap::new();
798 vars_two.insert("key", json!(1));
799 vars_two.insert("value2", json!(null));
800 let mut vars_one = JsonMap::new();
801 vars_one.insert("key1value2", json!(null));
802
803 let req_two = Request::fake_builder()
804 .subgraph_request(
805 http::Request::builder()
806 .body(graphql::Request::builder().variables(vars_two).build())
807 .unwrap(),
808 )
809 .build();
810 let req_one = Request::fake_builder()
811 .subgraph_request(
812 http::Request::builder()
813 .body(graphql::Request::builder().variables(vars_one).build())
814 .unwrap(),
815 )
816 .build();
817 let ignored_headers = HashSet::new();
818 assert_ne!(
819 req_two.to_sha256(&ignored_headers, false),
820 req_one.to_sha256(&ignored_headers, false),
821 "variable pairs must be delimited so concatenations cannot collide"
822 );
823 }
824
825 #[test]
826 fn test_subgraph_request_hash_no_cross_section_collision_variables_vs_extensions() {
827 use serde_json_bytes::json;
828
829 let mut vars = JsonMap::new();
836 vars.insert("k", json!(1));
837 let mut exts = JsonMap::new();
838 exts.insert("k", json!(1));
839
840 let req_vars_only = Request::fake_builder()
841 .subgraph_request(
842 http::Request::builder()
843 .body(graphql::Request::builder().variables(vars).build())
844 .unwrap(),
845 )
846 .build();
847 let req_exts_only = Request::fake_builder()
848 .subgraph_request(
849 http::Request::builder()
850 .body(graphql::Request::builder().extensions(exts).build())
851 .unwrap(),
852 )
853 .build();
854 let ignored_headers = HashSet::new();
855 assert_ne!(
856 req_vars_only.to_sha256(&ignored_headers, false),
857 req_exts_only.to_sha256(&ignored_headers, false),
858 "the variables and extensions sections must be domain-separated so identical \
859 entries in different sections cannot collide"
860 );
861 }
862
863 #[test]
864 fn test_subgraph_request_hash_no_cross_section_collision_query_vs_operation_name() {
865 let req_a = Request::fake_builder()
869 .subgraph_request(
870 http::Request::builder()
871 .body(
872 graphql::Request::builder()
873 .operation_name("AB")
874 .query("CD")
875 .build(),
876 )
877 .unwrap(),
878 )
879 .build();
880 let req_b = Request::fake_builder()
881 .subgraph_request(
882 http::Request::builder()
883 .body(
884 graphql::Request::builder()
885 .operation_name("ABC")
886 .query("D")
887 .build(),
888 )
889 .unwrap(),
890 )
891 .build();
892 let ignored_headers = HashSet::new();
893 assert_ne!(
894 req_a.to_sha256(&ignored_headers, false),
895 req_b.to_sha256(&ignored_headers, false),
896 "operation_name and query must be domain-separated so concatenations cannot collide"
897 );
898 }
899
900 #[test]
901 fn test_subgraph_request_hash_extensions_order_independence() {
902 use serde_json_bytes::json;
903
904 let mut ext_a = JsonMap::new();
905 ext_a.insert("alpha", json!("x"));
906 ext_a.insert("beta", json!("y"));
907 let mut ext_b = JsonMap::new();
908 ext_b.insert("beta", json!("y"));
909 ext_b.insert("alpha", json!("x"));
910
911 let req_a = Request::fake_builder()
912 .subgraph_request(
913 http::Request::builder()
914 .body(graphql::Request::builder().extensions(ext_a).build())
915 .unwrap(),
916 )
917 .build();
918 let req_b = Request::fake_builder()
919 .subgraph_request(
920 http::Request::builder()
921 .body(graphql::Request::builder().extensions(ext_b).build())
922 .unwrap(),
923 )
924 .build();
925 let ignored_headers = HashSet::new();
926 assert_eq!(
927 req_a.to_sha256(&ignored_headers, false),
928 req_b.to_sha256(&ignored_headers, false),
929 "two requests with the same extensions in different insertion orders must hash identically"
930 );
931 }
932
933 #[test]
934 fn test_subgraph_request_hash_header_order_independence() {
935 let req_a = Request::fake_builder()
936 .subgraph_request(
937 http::Request::builder()
938 .header("x-a", "1")
939 .header("x-b", "2")
940 .header("x-c", "3")
941 .body(graphql::Request::default())
942 .unwrap(),
943 )
944 .build();
945 let req_b = Request::fake_builder()
946 .subgraph_request(
947 http::Request::builder()
948 .header("x-c", "3")
949 .header("x-a", "1")
950 .header("x-b", "2")
951 .body(graphql::Request::default())
952 .unwrap(),
953 )
954 .build();
955 let ignored_headers = HashSet::new();
956 assert_eq!(
957 req_a.to_sha256(&ignored_headers, false),
958 req_b.to_sha256(&ignored_headers, false),
959 "two requests with the same headers in different insertion orders must hash identically"
960 );
961 }
962}