1use async_graphql::extensions::Analyzer;
2use async_graphql::{Context, EmptySubscription, ID, Object, Result as GqlResult, Schema};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8#[derive(Debug, thiserror::Error)]
9pub enum GraphQLError {
10 #[error("Schema error: {0}")]
11 Schema(String),
12 #[error("Resolver error: {0}")]
13 Resolver(String),
14 #[error("Not found: {0}")]
15 NotFound(String),
16}
17
18pub type GraphQLResult<T> = Result<T, GraphQLError>;
19
20pub const DEFAULT_MAX_QUERY_DEPTH: usize = 10;
25
26pub const DEFAULT_MAX_QUERY_COMPLEXITY: usize = 100;
31
32pub const DEFAULT_MAX_QUERY_SIZE: usize = 32_768; pub const DEFAULT_MAX_FIELD_COUNT: usize = 200;
42
43pub const DEFAULT_MAX_PAGE_SIZE: usize = 100;
47
48pub const DEFAULT_PAGE_SIZE: usize = 20;
50
51const MAX_NAME_LENGTH: usize = 100;
53
54const MAX_EMAIL_LENGTH: usize = 254;
56
57#[derive(Debug, Clone, Copy)]
80pub struct QueryLimits {
81 pub max_depth: usize,
83 pub max_complexity: usize,
85 pub max_query_size: usize,
87 pub max_field_count: usize,
89}
90
91impl QueryLimits {
92 pub fn new(max_depth: usize, max_complexity: usize) -> Self {
96 Self {
97 max_depth,
98 max_complexity,
99 max_query_size: DEFAULT_MAX_QUERY_SIZE,
100 max_field_count: DEFAULT_MAX_FIELD_COUNT,
101 }
102 }
103
104 pub fn full(
106 max_depth: usize,
107 max_complexity: usize,
108 max_query_size: usize,
109 max_field_count: usize,
110 ) -> Self {
111 Self {
112 max_depth,
113 max_complexity,
114 max_query_size,
115 max_field_count,
116 }
117 }
118}
119
120impl Default for QueryLimits {
121 fn default() -> Self {
122 Self {
123 max_depth: DEFAULT_MAX_QUERY_DEPTH,
124 max_complexity: DEFAULT_MAX_QUERY_COMPLEXITY,
125 max_query_size: DEFAULT_MAX_QUERY_SIZE,
126 max_field_count: DEFAULT_MAX_FIELD_COUNT,
127 }
128 }
129}
130
131pub fn validate_query(query: &str, limits: &QueryLimits) -> Result<(), String> {
136 if query.len() > limits.max_query_size {
138 return Err(format!(
139 "Query size {} bytes exceeds maximum of {} bytes",
140 query.len(),
141 limits.max_query_size
142 ));
143 }
144
145 let field_count = count_query_fields(query);
149 if field_count > limits.max_field_count {
150 return Err(format!(
151 "Query field count {} exceeds maximum of {}",
152 field_count, limits.max_field_count
153 ));
154 }
155
156 Ok(())
157}
158
159fn count_query_fields(query: &str) -> usize {
164 let mut count = 0;
166 let mut in_string = false;
167 let mut depth: usize = 0;
168
169 for line in query.lines() {
170 let trimmed = line.trim();
171 if trimmed.is_empty() || trimmed.starts_with('#') {
172 continue;
173 }
174
175 for ch in trimmed.chars() {
176 match ch {
177 '"' => in_string = !in_string,
178 '{' if !in_string => depth += 1,
179 '}' if !in_string => depth = depth.saturating_sub(1),
180 _ => {}
181 }
182 }
183
184 if depth > 0 && !in_string {
186 let field_line = trimmed.trim_start_matches('{').trim();
187 if !field_line.is_empty()
188 && !field_line.starts_with('}')
189 && !field_line.starts_with("...")
190 && !field_line.starts_with("query")
191 && !field_line.starts_with("mutation")
192 && !field_line.starts_with("subscription")
193 && !field_line.starts_with("fragment")
194 {
195 count += 1;
196 }
197 }
198 }
199
200 count
201}
202
203fn validate_create_user_input(input: &CreateUserInput) -> GqlResult<()> {
211 let name = input.name.trim();
213 if name.is_empty() {
214 return Err(async_graphql::Error::new("Name cannot be empty"));
215 }
216 if name.len() > MAX_NAME_LENGTH {
217 return Err(async_graphql::Error::new(format!(
218 "Name exceeds maximum length of {} characters",
219 MAX_NAME_LENGTH
220 )));
221 }
222 if !name
223 .chars()
224 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == ' ' || c == '.')
225 {
226 return Err(async_graphql::Error::new(
227 "Name contains invalid characters (allowed: alphanumeric, spaces, underscores, hyphens, dots)",
228 ));
229 }
230
231 let email = input.email.trim();
233 if email.is_empty() {
234 return Err(async_graphql::Error::new("Email cannot be empty"));
235 }
236 if email.len() > MAX_EMAIL_LENGTH {
237 return Err(async_graphql::Error::new(format!(
238 "Email exceeds maximum length of {} characters",
239 MAX_EMAIL_LENGTH
240 )));
241 }
242 let at_count = email.chars().filter(|c| *c == '@').count();
244 if at_count != 1 {
245 return Err(async_graphql::Error::new("Invalid email format"));
246 }
247 let parts: Vec<&str> = email.splitn(2, '@').collect();
248 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() || !parts[1].contains('.') {
249 return Err(async_graphql::Error::new("Invalid email format"));
250 }
251
252 Ok(())
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct User {
258 pub id: ID,
259 pub name: String,
260 pub email: String,
261 pub active: bool,
262}
263
264#[Object]
265impl User {
266 async fn id(&self) -> &ID {
267 &self.id
268 }
269
270 async fn name(&self) -> &str {
271 &self.name
272 }
273
274 async fn email(&self) -> &str {
275 &self.email
276 }
277
278 async fn active(&self) -> bool {
279 self.active
280 }
281}
282
283#[derive(Clone)]
285pub struct UserStorage {
286 users: Arc<RwLock<HashMap<String, User>>>,
287}
288
289impl UserStorage {
290 pub fn new() -> Self {
301 Self {
302 users: Arc::new(RwLock::new(HashMap::new())),
303 }
304 }
305 pub async fn add_user(&self, user: User) {
308 self.users.write().await.insert(user.id.to_string(), user);
309 }
310 pub async fn get_user(&self, id: &str) -> Option<User> {
319 self.users.read().await.get(id).cloned()
320 }
321 pub async fn list_users(&self) -> Vec<User> {
330 self.users.read().await.values().cloned().collect()
331 }
332}
333
334impl Default for UserStorage {
335 fn default() -> Self {
336 Self::new()
337 }
338}
339
340pub struct Query;
342
343#[Object]
344impl Query {
345 async fn user(&self, ctx: &Context<'_>, id: ID) -> GqlResult<Option<User>> {
346 let storage = ctx.data::<UserStorage>()?;
347 Ok(storage.get_user(id.as_ref()).await)
348 }
349
350 async fn users(
357 &self,
358 ctx: &Context<'_>,
359 first: Option<usize>,
360 offset: Option<usize>,
361 ) -> GqlResult<Vec<User>> {
362 let storage = ctx.data::<UserStorage>()?;
363 let limit = first
364 .unwrap_or(DEFAULT_PAGE_SIZE)
365 .min(DEFAULT_MAX_PAGE_SIZE);
366 let skip = offset.unwrap_or(0);
367 let all_users = storage.list_users().await;
368 Ok(all_users.into_iter().skip(skip).take(limit).collect())
369 }
370
371 async fn hello(&self, name: Option<String>) -> String {
372 format!("Hello, {}!", name.unwrap_or_else(|| "World".to_string()))
373 }
374}
375
376#[derive(async_graphql::InputObject)]
378pub struct CreateUserInput {
379 pub name: String,
380 pub email: String,
381}
382
383pub struct Mutation;
385
386#[Object]
387impl Mutation {
388 async fn create_user(&self, ctx: &Context<'_>, input: CreateUserInput) -> GqlResult<User> {
389 validate_create_user_input(&input)?;
391
392 let storage = ctx.data::<UserStorage>()?;
393
394 let user = User {
395 id: ID::from(uuid::Uuid::new_v4().to_string()),
396 name: input.name.trim().to_string(),
397 email: input.email.trim().to_string(),
398 active: true,
399 };
400
401 storage.add_user(user.clone()).await;
402 Ok(user)
403 }
404
405 async fn update_user_status(
406 &self,
407 ctx: &Context<'_>,
408 id: ID,
409 active: bool,
410 ) -> GqlResult<Option<User>> {
411 let storage = ctx.data::<UserStorage>()?;
412
413 if let Some(mut user) = storage.get_user(id.as_ref()).await {
414 user.active = active;
415 storage.add_user(user.clone()).await;
416 Ok(Some(user))
417 } else {
418 Ok(None)
419 }
420 }
421}
422
423pub type AppSchema = Schema<Query, Mutation, EmptySubscription>;
425
426pub fn create_schema(storage: UserStorage) -> AppSchema {
431 create_schema_with_limits(storage, QueryLimits::default())
432}
433
434pub fn create_schema_with_limits(storage: UserStorage, limits: QueryLimits) -> AppSchema {
444 Schema::build(Query, Mutation, EmptySubscription)
445 .data(storage)
446 .limit_depth(limits.max_depth)
447 .limit_complexity(limits.max_complexity)
448 .extension(Analyzer)
449 .finish()
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 #[tokio::test]
457 async fn test_query_hello() {
458 let storage = UserStorage::new();
459 let schema = create_schema(storage);
460
461 let query = r#"
462 {
463 hello(name: "GraphQL")
464 }
465 "#;
466
467 let result = schema.execute(query).await;
468 let data = result.data.into_json().unwrap();
469 assert_eq!(data["hello"], "Hello, GraphQL!");
470 }
471
472 #[tokio::test]
473 async fn test_mutation_create_user() {
474 let storage = UserStorage::new();
475 let schema = create_schema(storage);
476
477 let query = r#"
478 mutation {
479 createUser(input: { name: "Alice", email: "alice@example.com" }) {
480 name
481 email
482 active
483 }
484 }
485 "#;
486
487 let result = schema.execute(query).await;
488 let data = result.data.into_json().unwrap();
489 assert_eq!(data["createUser"]["name"], "Alice");
490 assert!(data["createUser"]["active"].as_bool().unwrap());
491 }
492
493 #[tokio::test]
494 async fn test_query_user() {
495 let storage = UserStorage::new();
496 let user = User {
497 id: ID::from("test-id-123"),
498 name: "Bob".to_string(),
499 email: "bob@example.com".to_string(),
500 active: true,
501 };
502 storage.add_user(user).await;
503
504 let schema = create_schema(storage);
505
506 let query = r#"
507 {
508 user(id: "test-id-123") {
509 id
510 name
511 email
512 active
513 }
514 }
515 "#;
516
517 let result = schema.execute(query).await;
518 let data = result.data.into_json().unwrap();
519 assert_eq!(data["user"]["id"], "test-id-123");
520 assert_eq!(data["user"]["name"], "Bob");
521 assert_eq!(data["user"]["email"], "bob@example.com");
522 assert!(data["user"]["active"].as_bool().unwrap());
523 }
524
525 #[tokio::test]
526 async fn test_query_user_not_found() {
527 let storage = UserStorage::new();
528 let schema = create_schema(storage);
529
530 let query = r#"
531 {
532 user(id: "nonexistent-id") {
533 id
534 name
535 }
536 }
537 "#;
538
539 let result = schema.execute(query).await;
540 let data = result.data.into_json().unwrap();
541 assert!(data["user"].is_null());
542 }
543
544 #[tokio::test]
545 async fn test_query_users_empty() {
546 let storage = UserStorage::new();
547 let schema = create_schema(storage);
548
549 let query = r#"
550 {
551 users {
552 id
553 name
554 }
555 }
556 "#;
557
558 let result = schema.execute(query).await;
559 let data = result.data.into_json().unwrap();
560 assert!(data["users"].is_array());
561 assert_eq!(data["users"].as_array().unwrap().len(), 0);
562 }
563
564 #[tokio::test]
565 async fn test_query_users_multiple() {
566 let storage = UserStorage::new();
567
568 let user1 = User {
569 id: ID::from("1"),
570 name: "Alice".to_string(),
571 email: "alice@example.com".to_string(),
572 active: true,
573 };
574 let user2 = User {
575 id: ID::from("2"),
576 name: "Bob".to_string(),
577 email: "bob@example.com".to_string(),
578 active: false,
579 };
580 let user3 = User {
581 id: ID::from("3"),
582 name: "Charlie".to_string(),
583 email: "charlie@example.com".to_string(),
584 active: true,
585 };
586
587 storage.add_user(user1).await;
588 storage.add_user(user2).await;
589 storage.add_user(user3).await;
590
591 let schema = create_schema(storage);
592
593 let query = r#"
594 {
595 users {
596 id
597 name
598 email
599 active
600 }
601 }
602 "#;
603
604 let result = schema.execute(query).await;
605 let data = result.data.into_json().unwrap();
606 let users = data["users"].as_array().unwrap();
607 assert_eq!(users.len(), 3);
608
609 let names: Vec<&str> = users.iter().map(|u| u["name"].as_str().unwrap()).collect();
611 assert!(names.contains(&"Alice"));
612 assert!(names.contains(&"Bob"));
613 assert!(names.contains(&"Charlie"));
614 }
615
616 #[tokio::test]
617 async fn test_query_users_pagination_with_first() {
618 let storage = UserStorage::new();
620 for i in 0..10 {
621 storage
622 .add_user(User {
623 id: ID::from(format!("user-{}", i)),
624 name: format!("User{}", i),
625 email: format!("user{}@example.com", i),
626 active: true,
627 })
628 .await;
629 }
630 let schema = create_schema(storage);
631
632 let query = r#"{ users(first: 3) { id } }"#;
634 let result = schema.execute(query).await;
635
636 assert!(result.errors.is_empty());
638 let data = result.data.into_json().unwrap();
639 let users = data["users"].as_array().unwrap();
640 assert_eq!(users.len(), 3);
641 }
642
643 #[tokio::test]
644 async fn test_query_users_pagination_with_offset() {
645 let storage = UserStorage::new();
647 for i in 0..5 {
648 storage
649 .add_user(User {
650 id: ID::from(format!("user-{}", i)),
651 name: format!("User{}", i),
652 email: format!("user{}@example.com", i),
653 active: true,
654 })
655 .await;
656 }
657 let schema = create_schema(storage);
658
659 let query = r#"{ users(first: 10, offset: 3) { id } }"#;
661 let result = schema.execute(query).await;
662
663 assert!(result.errors.is_empty());
665 let data = result.data.into_json().unwrap();
666 let users = data["users"].as_array().unwrap();
667 assert_eq!(users.len(), 2);
668 }
669
670 #[tokio::test]
671 async fn test_query_users_enforces_max_page_size() {
672 let storage = UserStorage::new();
674 for i in 0..150 {
675 storage
676 .add_user(User {
677 id: ID::from(format!("user-{}", i)),
678 name: format!("User{}", i),
679 email: format!("user{}@example.com", i),
680 active: true,
681 })
682 .await;
683 }
684 let schema = create_schema(storage);
685
686 let query = r#"{ users(first: 500) { id } }"#;
688 let result = schema.execute(query).await;
689
690 assert!(result.errors.is_empty());
692 let data = result.data.into_json().unwrap();
693 let users = data["users"].as_array().unwrap();
694 assert_eq!(users.len(), DEFAULT_MAX_PAGE_SIZE);
695 }
696
697 #[tokio::test]
698 async fn test_create_user_validates_empty_name() {
699 let storage = UserStorage::new();
701 let schema = create_schema(storage);
702
703 let query = r#"
705 mutation {
706 createUser(input: { name: " ", email: "test@example.com" }) {
707 id
708 }
709 }
710 "#;
711 let result = schema.execute(query).await;
712
713 assert!(
715 !result.errors.is_empty(),
716 "expected validation error for empty name"
717 );
718 }
719
720 #[tokio::test]
721 async fn test_create_user_validates_invalid_email() {
722 let storage = UserStorage::new();
724 let schema = create_schema(storage);
725
726 let query = r#"
728 mutation {
729 createUser(input: { name: "Alice", email: "not-an-email" }) {
730 id
731 }
732 }
733 "#;
734 let result = schema.execute(query).await;
735
736 assert!(
738 !result.errors.is_empty(),
739 "expected validation error for invalid email"
740 );
741 }
742
743 #[tokio::test]
744 async fn test_validate_query_rejects_oversized_query() {
745 let limits = QueryLimits::full(10, 100, 100, 200); let long_query = "{ ".to_string() + &"a ".repeat(100) + "}";
750 let result = validate_query(&long_query, &limits);
751
752 assert!(result.is_err());
754 assert!(result.unwrap_err().contains("exceeds maximum"));
755 }
756
757 #[tokio::test]
758 async fn test_validate_query_accepts_normal_query() {
759 let limits = QueryLimits::default();
761
762 let result = validate_query("{ users { id name } }", &limits);
764
765 assert!(result.is_ok());
767 }
768
769 #[tokio::test]
770 async fn test_mutation_update_user_status() {
771 let storage = UserStorage::new();
772 let user = User {
773 id: ID::from("update-test-id"),
774 name: "David".to_string(),
775 email: "david@example.com".to_string(),
776 active: true,
777 };
778 storage.add_user(user).await;
779
780 let schema = create_schema(storage);
781
782 let query = r#"
783 mutation {
784 updateUserStatus(id: "update-test-id", active: false) {
785 id
786 name
787 active
788 }
789 }
790 "#;
791
792 let result = schema.execute(query).await;
793 let data = result.data.into_json().unwrap();
794 assert_eq!(data["updateUserStatus"]["id"], "update-test-id");
795 assert!(!data["updateUserStatus"]["active"].as_bool().unwrap());
796 }
797
798 #[tokio::test]
799 async fn test_mutation_update_nonexistent_user() {
800 let storage = UserStorage::new();
801 let schema = create_schema(storage);
802
803 let query = r#"
804 mutation {
805 updateUserStatus(id: "does-not-exist", active: false) {
806 id
807 name
808 }
809 }
810 "#;
811
812 let result = schema.execute(query).await;
813 let data = result.data.into_json().unwrap();
814 assert!(data["updateUserStatus"].is_null());
815 }
816
817 #[tokio::test]
818 async fn test_user_object_fields() {
819 let user = User {
820 id: ID::from("field-test-id"),
821 name: "Eve".to_string(),
822 email: "eve@example.com".to_string(),
823 active: false,
824 };
825
826 assert_eq!(user.id.to_string(), "field-test-id");
828 assert_eq!(user.name, "Eve");
829 assert_eq!(user.email, "eve@example.com");
830 assert!(!user.active);
831 }
832
833 #[tokio::test]
834 async fn test_user_storage_add_get() {
835 let storage = UserStorage::new();
836
837 let user = User {
838 id: ID::from("storage-test-1"),
839 name: "Frank".to_string(),
840 email: "frank@example.com".to_string(),
841 active: true,
842 };
843
844 storage.add_user(user.clone()).await;
845
846 let retrieved = storage.get_user("storage-test-1").await;
847 let retrieved = retrieved.unwrap();
848 assert_eq!(retrieved.id.to_string(), "storage-test-1");
849 assert_eq!(retrieved.name, "Frank");
850 assert_eq!(retrieved.email, "frank@example.com");
851 assert!(retrieved.active);
852 }
853
854 #[tokio::test]
855 async fn test_user_storage_list() {
856 let storage = UserStorage::new();
857
858 let users = storage.list_users().await;
860 assert_eq!(users.len(), 0);
861
862 storage
864 .add_user(User {
865 id: ID::from("list-1"),
866 name: "User1".to_string(),
867 email: "user1@example.com".to_string(),
868 active: true,
869 })
870 .await;
871
872 storage
873 .add_user(User {
874 id: ID::from("list-2"),
875 name: "User2".to_string(),
876 email: "user2@example.com".to_string(),
877 active: false,
878 })
879 .await;
880
881 let users = storage.list_users().await;
882 assert_eq!(users.len(), 2);
883 }
884
885 #[tokio::test]
886 async fn test_create_schema_with_data() {
887 let storage = UserStorage::new();
888 storage
889 .add_user(User {
890 id: ID::from("pre-existing"),
891 name: "PreExisting".to_string(),
892 email: "preexisting@example.com".to_string(),
893 active: true,
894 })
895 .await;
896
897 let schema = create_schema(storage);
898
899 let query = r#"
901 {
902 user(id: "pre-existing") {
903 name
904 }
905 }
906 "#;
907
908 let result = schema.execute(query).await;
909 let data = result.data.into_json().unwrap();
910 assert_eq!(data["user"]["name"], "PreExisting");
911 }
912
913 #[tokio::test]
914 async fn test_graphql_error_types() {
915 let err1 = GraphQLError::Schema("test schema error".to_string());
916 assert!(err1.to_string().contains("Schema error"));
917
918 let err2 = GraphQLError::Resolver("test resolver error".to_string());
919 assert!(err2.to_string().contains("Resolver error"));
920
921 let err3 = GraphQLError::NotFound("test item".to_string());
922 assert!(err3.to_string().contains("Not found"));
923 }
924
925 #[tokio::test]
926 async fn test_query_depth_limit_rejects_deep_query() {
927 let storage = UserStorage::new();
929 let limits = QueryLimits::new(1, 1000);
930 let schema = create_schema_with_limits(storage, limits);
931
932 let query = r#"
934 {
935 users {
936 name
937 }
938 }
939 "#;
940 let result = schema.execute(query).await;
941
942 assert!(
944 !result.errors.is_empty(),
945 "expected depth limit error but query succeeded"
946 );
947 let error_message = &result.errors[0].message;
948 assert!(
949 error_message.to_lowercase().contains("too deep"),
950 "expected depth-limit message, got: {error_message}"
951 );
952 }
953
954 #[tokio::test]
955 async fn test_query_depth_limit_allows_shallow_query() {
956 let storage = UserStorage::new();
958 let limits = QueryLimits::new(10, 1000);
959 let schema = create_schema_with_limits(storage, limits);
960
961 let query = r#"{ hello(name: "Test") }"#;
963 let result = schema.execute(query).await;
964
965 assert!(
967 result.errors.is_empty(),
968 "expected no errors for shallow query"
969 );
970 let data = result.data.into_json().unwrap();
971 assert_eq!(data["hello"], "Hello, Test!");
972 }
973
974 #[tokio::test]
975 async fn test_query_complexity_limit_rejects_complex_query() {
976 let storage = UserStorage::new();
978 let limits = QueryLimits::new(100, 1);
979 let schema = create_schema_with_limits(storage, limits);
980
981 let query = r#"
983 {
984 users {
985 id
986 name
987 email
988 active
989 }
990 }
991 "#;
992 let result = schema.execute(query).await;
993
994 assert!(
996 !result.errors.is_empty(),
997 "expected complexity limit error but query succeeded"
998 );
999 let error_message = &result.errors[0].message;
1000 assert!(
1001 error_message.to_lowercase().contains("complex"),
1002 "expected complexity-limit message, got: {error_message}"
1003 );
1004 }
1005
1006 #[tokio::test]
1007 async fn test_query_limits_default_values() {
1008 let limits = QueryLimits::default();
1010
1011 assert_eq!(limits.max_depth, DEFAULT_MAX_QUERY_DEPTH);
1013 assert_eq!(limits.max_complexity, DEFAULT_MAX_QUERY_COMPLEXITY);
1014 }
1015
1016 #[tokio::test]
1017 async fn test_create_schema_with_custom_limits() {
1018 let storage = UserStorage::new();
1020 let limits = QueryLimits::new(20, 500);
1021 let schema = create_schema_with_limits(storage, limits);
1022
1023 let query = r#"{ hello }"#;
1025 let result = schema.execute(query).await;
1026
1027 assert!(result.errors.is_empty());
1029 let data = result.data.into_json().unwrap();
1030 assert_eq!(data["hello"], "Hello, World!");
1031 }
1032
1033 #[tokio::test]
1034 async fn test_analyzer_extension_present() {
1035 let storage = UserStorage::new();
1037 let schema = create_schema(storage);
1038
1039 let query = r#"{ hello(name: "Analyzer") }"#;
1041 let result = schema.execute(query).await;
1042
1043 assert!(result.errors.is_empty());
1045 assert!(
1046 !result.extensions.is_empty(),
1047 "expected Analyzer extension data in response"
1048 );
1049 }
1050}