Coverage for graphqler / fuzzer / engine / retrier / retrier.py: 82%

33 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-20 10:09 -0400

1"""Retrier 

2This moule will retry any immediate errors that arise during the query. This is not responsible for running the same query / mutation again, 

3rather, it's responsible for modifying the query / mutation to make it work. Scenarios: 

4- We have a NON-NULL column that is selected for in the output, but the server is responding with NULL, you will get the following error: 

5 {'message': 'Cannot return null for non-nullable field Transaction.payer.'}. 

6 In this scenario, we will need to remove the payer key from the mutation / query output fields 

7""" 

8 

9from graphqler.fuzzer.engine.retrier.utils import find_block_end, remove_lines_within_range 

10from graphqler.utils import plugins_handler 

11import logging 

12 

13 

14class Retrier: 

15 def __init__(self, logger: logging.Logger): 

16 self.logger = logger.getChild(__name__) 

17 self.max_retries = 3 

18 

19 def retry(self, url: str, payload: str | dict | list, gql_response: dict, retry_count) -> tuple[dict, bool]: 

20 """Retries the payload based on the error 

21 

22 Args: 

23 url (str): The url of the endpoint 

24 payload (str): The payload (either a query or mutation) 

25 gql_response (dict): The GraphQL response containing the error 

26 retry_count (int): The number of times we've retried 

27 

28 Returns: 

29 tuple[dict, bool]: The response, and whether the retry succeeded or not 

30 """ 

31 error = gql_response["errors"][0] 

32 

33 if isinstance(payload, dict) or isinstance(payload, list): 

34 return (gql_response, False) 

35 

36 # If the error doesn't have a message, we can't do anything to fix it 

37 if "message" not in error: 

38 return (gql_response, False) 

39 

40 if "Cannot return null for non-nullable field" in error["message"] or "Field must have selections" in error["message"]: 

41 if "locations" not in error: 

42 return (gql_response, False) 

43 locations = error["locations"] 

44 for location in locations: 

45 payload = self.get_new_payload_for_retry_non_null(payload, location) 

46 self.logger.debug(f"Retrying with new payload:\n {payload}") 

47 gql_response, request_response = plugins_handler.get_request_utils().send_graphql_request(url, payload) 

48 self.logger.info(f"Response: {gql_response}") 

49 if "errors" in gql_response: 

50 if retry_count < self.max_retries: 

51 return self.retry(url, payload, gql_response, retry_count + 1) 

52 else: 

53 return (gql_response, False) 

54 else: 

55 return (gql_response, True) 

56 else: 

57 return (gql_response, False) 

58 

59 def get_new_payload_for_retry_non_null(self, payload: str, location: dict) -> str: 

60 """Gets a new payload from the original payload, and the location of the error 

61 

62 Args: 

63 payload (str): The payload 

64 error (dict): The error 

65 

66 Returns: 

67 str: A string of the new payload 

68 """ 

69 line_number = location["line"] 

70 block_end = find_block_end(payload, line_number - 1) 

71 new_payload = remove_lines_within_range(payload, line_number - 1, block_end) 

72 return new_payload