Skip to content

Conversation

@kirkmorrow
Copy link
Contributor

@kirkmorrow kirkmorrow commented Dec 6, 2025

Pull Request

Issue

Closes: #1043

Approach

Implemented automatic retry mechanism for network operations at the HTTP client layer to handle transient network failures and HTML error responses from reverse proxies/load balancers.

Core Implementation:

  • Created parse_network_retry.dart with executeWithRetry() function that wraps HTTP operations
  • Integrated retry logic into both ParseHTTPClient and ParseDioClient by wrapping all 6 HTTP methods (get, post, put, delete, head, patch)
  • Enhanced ParseErrorResponse to detect and handle HTML responses

Retry Conditions:

  • Status code -1 (network/connection errors)
  • HTML responses instead of expected JSON (e.g., 502 Bad Gateway, 503 Service Unavailable)
  • Exceptions thrown during HTTP operations
  • Does NOT retry valid Parse Server errors (e.g., 101 ObjectNotFound, 209 InvalidSessionToken)

Configuration:

  • Added restRetryIntervals parameter to Parse().initialize()
  • Default: [0, 250, 500, 1000, 2000] (5 retry attempts with exponential backoff)
  • Fully customizable by developers
  • Backward compatible - works with default settings

Testing:

  • 26 unit tests covering all retry scenarios and edge cases (100% coverage of core retry logic)
  • 7 integration tests documenting behavior with MockClient
  • All 200 existing tests continue to pass

Documentation:

  • Inline code documentation explaining retry behavior
  • Coverage documentation explaining expected low coverage in HTTP client wrappers (retry happens at deeper layer)
  • Integration tests serve as documentation for MockClient behavior

Tasks

  • Add tests
  • Add changes to documentation (guides, repository pages, code comments)

Summary by CodeRabbit

  • New Features

    • Configurable REST retry intervals for reads and writes via initialization; automatic retry wrapper applied across HTTP clients with capped attempts and optional debug logging.
  • Bug Fixes

    • Standardized structured JSON error payloads and improved handling/clearer messages for non-JSON or malformed error responses.
  • Tests

    • New unit and integration tests covering retry behavior, timing, read-vs-write differences, non-retryable cases, and mock-client behavior.
  • Documentation

    • Added descriptive headers and notes describing the retry-focused architecture and coverage.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 6, 2025 23:15
@parse-github-assistant
Copy link

🚀 Thanks for opening this pull request!

@coderabbitai
Copy link

coderabbitai bot commented Dec 6, 2025

Warning

Rate limit exceeded

@kirkmorrow has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 11 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 7e8623d and a7271bb.

📒 Files selected for processing (1)
  • packages/dart/lib/parse_server_sdk.dart (4 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a centralized executeWithRetry retry mechanism, surfaces configurable REST retry intervals (separate read/write) via Parse.initialize → ParseCoreData, applies retry wrappers to HTTP clients (Dio/Http), standardizes error JSON handling, improves non‑JSON error parsing, and adds related unit and integration tests.

Changes

Cohort / File(s) Summary
Library init
packages/dart/lib/parse_server_sdk.dart
Added parameters List<int>? restRetryIntervals and List<int>? restRetryIntervalsForWrites to Parse.initialize, forwarded them to ParseCoreData.init(...), and added part 'src/network/parse_network_retry.dart';.
Core data
packages/dart/lib/src/data/parse_core_data.dart
Added fields late List<int> restRetryIntervals and late List<int> restRetryIntervalsForWrites; extended init(...) to accept and initialize those optional parameters (with defaults when absent).
Network retry core
packages/dart/lib/src/network/parse_network_retry.dart
New file introducing Future<T> executeWithRetry<T extends ParseNetworkResponse>({ required Future<T> Function() operation, bool isWriteOperation = false, bool? debug }) implementing retry loop, selecting read/write intervals from ParseCoreData, enforcing a 100-interval cap, retry eligibility checks, delays, and debug logging.
Dio HTTP client
packages/dart/lib/src/network/parse_dio_client.dart
Wrapped HTTP methods with executeWithRetry(...) (write ops use isWriteOperation: true), refactored try/catch into operation closures, standardized error payloads via _buildErrorJson, and unified fallback error handling; public signatures unchanged.
Http client
packages/dart/lib/src/network/parse_http_client.dart
Same changes as Dio client: HTTP methods now call executeWithRetry(...), added _buildErrorJson, and standardized error return shapes; public signatures unchanged.
Error parsing
packages/dart/lib/src/objects/response/parse_error_response.dart
Wrapped JSON decode in try/catch; on FormatException produce a previewed non‑JSON error object and set response.statusCode/error to ParseError.otherCause.
Tests — retry unit tests
packages/dart/test/src/network/parse_network_retry_test.dart
New comprehensive unit tests for executeWithRetry: success, retries on -1/HTML responses, exception retries, custom intervals, read vs write interval behavior, timing/delay checks, byte responses, and 100-retry cap validation.
Tests — client integration
packages/dart/test/src/network/parse_client_retry_integration_test.dart
New mocked-client integration tests ensuring HTTP-layer retry is not applied when ParseClient is mocked (verifies single call invocation and expected response behavior).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Client
    participant SDK as Parse HTTP Client
    participant Retry as executeWithRetry
    participant Server as Parse Server

    App->>SDK: perform HTTP operation (get/post/put/delete)
    SDK->>Retry: executeWithRetry(operation: () => actual HTTP call, isWriteOperation?)
    Retry->>SDK: invoke operation attempt `#1`
    SDK->>Server: send HTTP request
    alt network failure / HTML-like / status -1
        Server-->>SDK: error / HTML / no-response
        SDK-->>Retry: return retryable ParseNetworkResponse (status -1 / otherCause)
        Retry-->>Retry: wait interval (from ParseCoreData().restRetryIntervals or restRetryIntervalsForWrites)
        Retry->>SDK: invoke operation attempt `#2` (repeat per intervals)
    else success (200/201) or non-retriable Parse error
        Server-->>SDK: success or non-retriable error
        SDK-->>Retry: return final ParseNetworkResponse
    end
    Retry-->>App: final ParseNetworkResponse (success or error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Potential attention areas:

  • Correctness of executeWithRetry: retry eligibility logic, interval indexing, and 100-interval cap.
  • Separation and plumbing of read vs write retry interval lists.
  • Preservation of original Dio exception → Parse error mappings and consistent error JSON shapes.
  • Timing-sensitive tests (may be flaky); verify determinism and test durations.

Suggested reviewers

  • mtrezza
  • mbfakourii

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: implementing a REST API retry mechanism, which is the primary objective of this pull request.
Description check ✅ Passed The description is comprehensive, following the template structure with all required sections (Issue, Approach, Tasks) completed with detailed implementation details, configuration, testing coverage, and documentation.
Linked Issues check ✅ Passed The PR implementation fully satisfies issue #1043 requirements: configurable retry mechanism via restRetryIntervals, handles transient network failures and HTML errors, respects Parse error semantics, and includes comprehensive testing.
Out of Scope Changes check ✅ Passed All changes directly support the retry mechanism objective. No unrelated modifications detected; changes span network retry logic, HTTP client integration, error response handling, configuration, and corresponding tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Dec 6, 2025

Codecov Report

❌ Patch coverage is 24.10256% with 148 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.37%. Comparing base (9093dbd) to head (a7271bb).

Files with missing lines Patch % Lines
...ackages/dart/lib/src/network/parse_dio_client.dart 0.00% 79 Missing ⚠️
...ckages/dart/lib/src/network/parse_http_client.dart 0.00% 67 Missing ⚠️
...ages/dart/lib/src/network/parse_network_retry.dart 96.96% 1 Missing ⚠️
...lib/src/objects/response/parse_error_response.dart 91.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1102      +/-   ##
==========================================
+ Coverage   44.26%   44.37%   +0.10%     
==========================================
  Files          61       62       +1     
  Lines        3637     3725      +88     
==========================================
+ Hits         1610     1653      +43     
- Misses       2027     2072      +45     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements an automatic retry mechanism for REST API network operations to handle transient failures from network issues, reverse proxies, and load balancers. The retry logic operates at the HTTP client layer with configurable exponential backoff.

Key Changes:

  • Added executeWithRetry() function that wraps HTTP operations with automatic retry capability for status code -1 (network errors) and HTML error responses
  • Enhanced error response handling to gracefully manage non-JSON responses from proxies/load balancers
  • Introduced restRetryIntervals configuration parameter with sensible defaults [0, 250, 500, 1000, 2000]

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/dart/lib/src/network/parse_network_retry.dart Core retry logic implementing exponential backoff with configurable intervals and HTML error detection
packages/dart/lib/src/network/parse_http_client.dart Wraps all HTTP methods (get, post, put, delete, head, patch) with executeWithRetry() and standardizes error formatting
packages/dart/lib/src/network/parse_dio_client.dart Wraps all Dio HTTP methods with executeWithRetry() and updates _fallbackErrorData to include error code
packages/dart/lib/src/objects/response/parse_error_response.dart Adds FormatException handling to gracefully process HTML responses with helpful error messages
packages/dart/lib/src/data/parse_core_data.dart Adds restRetryIntervals field with default configuration
packages/dart/lib/parse_server_sdk.dart Exposes restRetryIntervals parameter in Parse.initialize() and registers retry module
packages/dart/test/src/network/parse_network_retry_test.dart Comprehensive unit tests (26 tests) covering all retry scenarios, edge cases, and configuration validation
packages/dart/test/src/network/parse_client_retry_integration_test.dart Integration tests (7 tests) documenting MockClient behavior and architectural design decisions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
packages/dart/test/src/network/parse_client_retry_integration_test.dart (1)

147-154: Verify the fetch test assertion logic.

The test expects fetchedObject.objectId to be 'abc123' after a network error response. This is checking that the original object's objectId is preserved, not that the fetch succeeded. Consider adding an assertion on response.success or the error to make the test intent clearer.

        final object = ParseObject('TestObject', client: client)
          ..objectId = 'abc123';
-        final fetchedObject = await object.fetch();
+        final response = await object.fetch();

        expect(callCount, 1); // No retry via mock
-        expect(fetchedObject.objectId, 'abc123');
+        expect(response.success, false);
+        expect(object.objectId, 'abc123'); // Original objectId preserved
packages/dart/test/src/network/parse_network_retry_test.dart (2)

147-173: Potential race condition in exception test.

The expect(..., throwsA(...)) returns once the exception is thrown, but the callCount is checked after a Future.delayed(50ms). Since the retry intervals are [0, 10], the total delay should be ~10ms, but if the test execution is slow, this could be flaky.

Consider restructuring to await all retries before checking:

    test('should handle exceptions and retry', () async {
      int callCount = 0;
      final oldIntervals = ParseCoreData().restRetryIntervals;
      ParseCoreData().restRetryIntervals = [0, 10];

-      expect(
-        () async => await executeWithRetry(
+      await expectLater(
+        executeWithRetry(
          operation: () async {
            callCount++;
            throw Exception('Network timeout');
          },
        ),
        throwsA(
          isA<Exception>().having(
            (e) => e.toString(),
            'message',
            contains('Network timeout'),
          ),
        ),
      );

      // Should retry on exceptions: initial + 2 retries = 3 times
-      await Future.delayed(Duration(milliseconds: 50)); // Wait for retries
      expect(callCount, 3);

      ParseCoreData().restRetryIntervals = oldIntervals;
    });

522-528: Test doesn't verify actual implementation behavior.

This test only validates a static string format rather than testing that both HTTP clients produce the same error format. Consider removing it or replacing with a more meaningful test that actually exercises both clients.

packages/dart/lib/src/network/parse_network_retry.dart (3)

67-74: Clarify the max retries error message.

The error message states "cannot exceed $maxRetries retries" but the actual number of attempts is retryIntervals.length + 1 (one initial attempt plus retries). This could confuse users about the limit. Consider rewording for clarity:

   if (retryIntervals.length > maxRetries) {
     throw ArgumentError(
-      'restRetryIntervals cannot exceed $maxRetries retries. '
+      'restRetryIntervals cannot exceed $maxRetries retry attempts. '
       'Current length: ${retryIntervals.length}',
     );
   }

Or more precisely:

   if (retryIntervals.length > maxRetries) {
     throw ArgumentError(
-      'restRetryIntervals cannot exceed $maxRetries retries. '
+      'restRetryIntervals cannot exceed $maxRetries elements (which allows up to ${maxRetries + 1} total attempts). '
       'Current length: ${retryIntervals.length}',
     );
   }

139-141: Consider throwing instead of using non-null assertion.

While this code should be unreachable, the non-null assertion operator ! could cause issues if the compiler cannot prove lastResponse is non-null. Consider throwing an error for clarity and safety:

-  // Should never reach here, but return last response as fallback
-  return lastResponse!;
+  // This should never be reached due to the loop logic above
+  throw StateError(
+    'Retry loop completed without returning or rethrowing. This indicates a logic error.',
+  );

This makes the "impossible" case explicit and easier to debug if it somehow occurs.


157-175: HTML detection optimization is optional; ParseError.otherCause value is correct.

The check for ParseError.otherCause == -1 is correct. However, the suggested optimization to substring the response body has tradeoffs worth considering:

The current code calls trimLeft() and toLowerCase() on the full response. If you want to optimize for large responses, you could limit the check to the first ~100 characters:

-  final String trimmedData = response.data.trimLeft().toLowerCase();
+  // Only check first ~100 chars for HTML markers (proxy errors typically at the start)
+  final String prefix = response.data
+      .substring(0, response.data.length < 100 ? response.data.length : 100)
+      .trimLeft()
+      .toLowerCase();

   // Check for common HTML patterns that indicate proxy/load balancer errors
   // More robust than just checking for '<' which could be in valid JSON strings
-  if (trimmedData.startsWith('<!doctype') ||
-      trimmedData.startsWith('<html') ||
-      trimmedData.startsWith('<head') ||
-      trimmedData.startsWith('<body')) {
+  if (prefix.startsWith('<!doctype') ||
+      prefix.startsWith('<html') ||
+      prefix.startsWith('<head') ||
+      prefix.startsWith('<body')) {
     // HTML response indicates proxy/load balancer error
     return true;
   }

Note: substring() always allocates a new string in Dart, while trimLeft() and toLowerCase() only allocate if changes are needed. For typical proxy error pages (small, starting with whitespace or HTML), the optimization may have negligible impact. Apply only if profiling shows large response bodies are common in your retry scenarios.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9093dbd and 22fde31.

📒 Files selected for processing (8)
  • packages/dart/lib/parse_server_sdk.dart (3 hunks)
  • packages/dart/lib/src/data/parse_core_data.dart (3 hunks)
  • packages/dart/lib/src/network/parse_dio_client.dart (7 hunks)
  • packages/dart/lib/src/network/parse_http_client.dart (7 hunks)
  • packages/dart/lib/src/network/parse_network_retry.dart (1 hunks)
  • packages/dart/lib/src/objects/response/parse_error_response.dart (1 hunks)
  • packages/dart/test/src/network/parse_client_retry_integration_test.dart (1 hunks)
  • packages/dart/test/src/network/parse_network_retry_test.dart (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Test Dart 3.10, Ubuntu
  • GitHub Check: Test Dart 3.10, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Dart beta
  • GitHub Check: Test Dart 3.10, macOS
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Auto-Release Dry Run
  • GitHub Check: Agent
🔇 Additional comments (12)
packages/dart/lib/src/objects/response/parse_error_response.dart (1)

8-30: Good defensive handling for non-JSON responses.

The try-catch structure properly handles HTML responses from reverse proxies/load balancers by creating a meaningful error message with a preview. The exception is correctly attached to the ParseError for debugging.

One minor consideration: if apiResponse.data is empty, the preview will just be an empty string, which is fine but produces a slightly awkward message like "Invalid response format (expected JSON): ". This is acceptable behavior but could be enhanced later.

packages/dart/lib/src/network/parse_http_client.dart (1)

50-69: Good retry wrapper integration.

The executeWithRetry wrapper is correctly applied to all HTTP methods with consistent error handling patterns. The structure properly separates the HTTP operation logic from the retry mechanism.

packages/dart/test/src/network/parse_client_retry_integration_test.dart (1)

9-34: Excellent documentation of testing architecture.

The documentation clearly explains why mock tests don't exercise retry logic and how the testing strategy is split between unit tests (for retry logic) and integration tests (for ParseClient interface behavior). This will help future maintainers understand the coverage implications.

packages/dart/test/src/network/parse_network_retry_test.dart (1)

11-283: Comprehensive retry logic coverage.

The test suite thoroughly covers:

  • Immediate success scenarios (200, 201)
  • Non-retriable Parse errors (101)
  • Retry on status -1 and HTML responses
  • Exception handling and recovery
  • Delay timing verification
  • Configuration validation (max 100 retries)
  • Both response types (ParseNetworkResponse, ParseNetworkByteResponse)

The save/restore pattern for restRetryIntervals properly isolates tests.

packages/dart/lib/src/data/parse_core_data.dart (1)

35-36: Clean integration of retry configuration.

The new restRetryIntervals parameter follows the existing pattern for liveListRetryIntervals. The default [0, 250, 500, 1000, 2000] provides sensible exponential backoff (5 total attempts including the initial call).

The mutable field design allows runtime configuration changes, which is useful for testing but also means the configuration can be altered during app execution. This is consistent with the existing liveListRetryIntervals behavior.

Also applies to: 56-57, 95-95

packages/dart/lib/src/network/parse_dio_client.dart (2)

39-58: Consistent retry wrapper integration across all HTTP methods.

The executeWithRetry wrapper is correctly applied to all Dio HTTP operations. The error handling properly distinguishes between responses with and without error data, falling back to _fallbackErrorData when needed.

Also applies to: 109-129, 138-158, 212-231


234-235: Clean fallback error data.

The _fallbackErrorData getter provides a well-formed JSON string for network errors. This is correctly used as a fallback when DioException doesn't have response data.

packages/dart/lib/src/network/parse_network_retry.dart (2)

76-137: LGTM: Retry loop logic is sound.

The retry loop correctly handles:

  • Initial attempt plus configurable retries based on retryIntervals.length
  • Proper bounds checking before accessing retryIntervals[i]
  • Early return on success or non-retryable errors
  • Exception handling with rethrow on final attempt
  • Helpful debug logging for troubleshooting

The logic ensures that with retryIntervals of length N, the operation is attempted N+1 times total (1 initial + N retries), which aligns with the expected behavior.


25-37: The retry mechanism for POST/PUT operations is documented with appropriate risk mitigations.

The documentation correctly identifies that POST/PUT retries carry a duplication risk if the response is lost. The stated mitigations—optimistic locking, idempotent-by-design operations, and network-level-only retry triggers—are standard Parse Server safeguards. The documentation sufficiently informs developers of the tradeoff. No additional verification or changes needed unless there are specific use cases in your application where this retry behavior is inappropriate (in which case, that should be handled at the application integration layer, not in the SDK).

packages/dart/lib/parse_server_sdk.dart (3)

39-39: LGTM: Part directive correctly added.

The new part directive for parse_network_retry.dart is properly placed among other network-related part files.


131-149: Review comment cannot be verified due to unavailable repository access.

The repository could not be cloned to verify the following claims in the original review comment:

  • That ParseCoreData.init() accepts the restRetryIntervals parameter
  • That the default value [0, 250, 500, 1000, 2000] is properly set when null is provided
  • That the parameter is correctly passed through to ParseCoreData.init()

Manual verification of the ParseCoreData.init() method signature and implementation is required to confirm these details.


104-122: Add documentation for the restRetryIntervals parameter in the method's doc comment.

The restRetryIntervals parameter (line 122) is correctly typed and positioned, but it must be documented in the Future<Parse> initialize() method's doc comment (lines 92-103) to maintain consistency with other parameters.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/dart/lib/src/network/parse_network_retry.dart (2)

139-140: Unreachable code assertion is correct but could be more explicit.

The control flow ensures this line is never reached: all paths either return early or rethrow. Consider using an assertion or more explicit comment to document this invariant for maintainability.

-  // Should never reach here, but return last response as fallback
-  return lastResponse!;
+  // Unreachable: all paths return or rethrow in the loop
+  assert(false, 'Unreachable code - loop should always return or rethrow');
+  return lastResponse!;

157-179: HTML detection logic is redundant within the current control flow.

Lines 165-171 check for HTML patterns and return true, but line 174 also returns true unconditionally for all -1 status codes. The HTML check never changes the outcome.

If the intent is simply to retry all -1 errors (which is reasonable), simplify:

 bool _shouldRetryResponse(ParseNetworkResponse response) {
-  // Retry on status code -1 (network/parse errors)
-  if (response.statusCode == ParseError.otherCause) {
-    // Additional check: is it HTML instead of JSON?
-    final String trimmedData = response.data.trimLeft().toLowerCase();
-
-    // Check for common HTML patterns that indicate proxy/load balancer errors
-    // More robust than just checking for '<' which could be in valid JSON strings
-    if (trimmedData.startsWith('<!doctype') ||
-        trimmedData.startsWith('<html') ||
-        trimmedData.startsWith('<head') ||
-        trimmedData.startsWith('<body')) {
-      // HTML response indicates proxy/load balancer error
-      return true;
-    }
-
-    // Other -1 errors (network issues, parse failures)
-    return true;
-  }
-
-  // Don't retry successful responses or valid Parse API errors
-  return false;
+  // Retry on status code -1 (network/parse errors, including HTML from proxies)
+  return response.statusCode == ParseError.otherCause;
 }

Alternatively, if HTML detection should influence different behavior (e.g., logging or future logic), keep it but make the distinction meaningful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22fde31 and bb98ddd.

📒 Files selected for processing (1)
  • packages/dart/lib/src/network/parse_network_retry.dart (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test Dart 3.10, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Flutter 3.38, Ubuntu
  • GitHub Check: Test Flutter 3.38, macOS
🔇 Additional comments (4)
packages/dart/lib/src/network/parse_network_retry.dart (4)

1-59: Well-documented retry mechanism with clear idempotency warnings.

The documentation comprehensively covers retry conditions, edge cases, and importantly warns about non-idempotent operations (POST/PUT). The explanation of why this is acceptable in Parse Server scenarios is helpful.


60-74: Good defensive programming with retry limit enforcement.

The 100-retry cap prevents configuration mistakes from causing excessive network requests. The generic constraint T extends ParseNetworkResponse ensures type safety while supporting both ParseNetworkResponse and ParseNetworkByteResponse.


76-115: Retry loop implementation is correct and well-structured.

The loop correctly handles:

  • Initial attempt plus configured retry count
  • Early return on success or non-retryable errors
  • Proper delay timing using the intervals array
  • Debug logging with accurate attempt counts

The exponential backoff pattern with configurable intervals is a good practice for handling transient failures.


161-161: Verify response.data is guaranteed to be a non-null String.

Calling response.data.trimLeft() will throw if data is null or not a String. Ensure ParseNetworkResponse.data is always a non-nullable String when statusCode == -1.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dart/lib/src/network/parse_dio_client.dart (1)

162-199: Stream data cannot be safely retried - potential data loss.

The postBytes method accepts a Stream<List<int>> for the request body. Streams can only be consumed once. If a transient failure triggers a retry, the stream will be exhausted, and the retry will send an empty or incomplete body.

This is a significant issue for file uploads or other stream-based operations that rely on retry for reliability.

Consider one of these approaches:

  1. Buffer the stream to a List<int> before entering the retry loop
  2. Accept a factory function Stream<List<int>> Function() that creates fresh streams
  3. Document that postBytes does not support retry and exclude it from executeWithRetry
  @override
  Future<ParseNetworkResponse> postBytes(
    String path, {
    Stream<List<int>>? data,
    ParseNetworkOptions? options,
    ProgressCallback? onSendProgress,
    dynamic cancelToken,
  }) async {
+   // Note: Stream-based requests cannot be reliably retried since streams
+   // can only be consumed once. Consider buffering if retry is needed.
    return executeWithRetry(
🧹 Nitpick comments (5)
packages/dart/test/src/network/parse_client_retry_integration_test.dart (1)

147-153: Consider adding response.success assertion for consistency.

Other tests (lines 72, 100, 124, 176, 204) verify response.success, but this fetch test only checks objectId preservation. While fetch() returns the object directly, adding a success check would maintain consistency across tests.

         final fetchedObject = await object.fetch();
 
         expect(callCount, 1); // No retry via mock
         expect(fetchedObject.objectId, 'abc123'); // Original objectId preserved
+        // Note: fetch() returns ParseObject, not ParseResponse - success check not applicable
       },
packages/dart/test/src/network/parse_network_retry_test.dart (3)

67-92: Test isolation pattern is correct but verbose - consider a helper.

The save/restore pattern for restRetryIntervals is repeated across most tests. While correct for test isolation, consider extracting a test utility or using setUp/tearDown with a stored value to reduce boilerplate.

Example helper approach:

T withRetryIntervals<T>(List<int> intervals, T Function() testBody) {
  final old = ParseCoreData().restRetryIntervals;
  try {
    ParseCoreData().restRetryIntervals = intervals;
    return testBody();
  } finally {
    ParseCoreData().restRetryIntervals = old;
  }
}

198-218: Timing-sensitive test may be flaky in CI environments.

The test expects at least 250ms for a 300ms total delay (100+200). While there's a 50ms tolerance, CI environments with resource contention may occasionally fail. Consider increasing the tolerance or using a fake clock for more deterministic testing.

       // Should have at least 300ms delay (100 + 200)
       // Allow some variance for test execution
-      expect(duration.inMilliseconds, greaterThan(250));
+      expect(duration.inMilliseconds, greaterThan(200)); // Allow more variance for CI

285-294: This test provides no behavioral validation.

As the comment notes, this test cannot directly test the private function. The assertion only verifies the response object's properties, not the retry behavior. Consider removing this test since the behavior is already validated by other tests in this file, or convert it to a documentation comment.

packages/dart/lib/src/network/parse_network_retry.dart (1)

147-167: Documentation mentions HTML detection but implementation only checks status code.

The docstring for _shouldRetryResponse mentions "HTML responses from proxies/load balancers" as a retry trigger, but the implementation only checks statusCode == ParseError.otherCause.

If HTML responses are being converted to status code -1 elsewhere (e.g., in the HTTP client or error parsing), the documentation is misleading about what this specific function does. Consider clarifying that HTML detection happens at the response parsing layer, not in this function.

 /// Determines if a network response should be retried.
 ///
 /// Returns `true` if the response indicates a transient network error
 /// that might succeed on retry, `false` otherwise.
 ///
 /// Retry Triggers:
 ///
-/// - Status code `-1` (network/parsing errors)
-///   - HTML responses from proxies/load balancers (502, 503, 504 errors)
-///   - Socket exceptions, timeouts, DNS failures
-///   - JSON parse errors from malformed responses
+/// - Status code `-1` (network/parsing errors from the HTTP client layer)
+///   Note: HTML responses, socket exceptions, timeouts, and parse errors
+///   are converted to status -1 by the HTTP client before reaching here.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb98ddd and 5096a9e.

📒 Files selected for processing (5)
  • packages/dart/lib/src/network/parse_dio_client.dart (6 hunks)
  • packages/dart/lib/src/network/parse_http_client.dart (7 hunks)
  • packages/dart/lib/src/network/parse_network_retry.dart (1 hunks)
  • packages/dart/test/src/network/parse_client_retry_integration_test.dart (1 hunks)
  • packages/dart/test/src/network/parse_network_retry_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/dart/lib/src/network/parse_http_client.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter 3.38, Ubuntu
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter beta
🔇 Additional comments (5)
packages/dart/test/src/network/parse_client_retry_integration_test.dart (1)

1-38: Well-documented integration test setup with clear architectural rationale.

The documentation clearly explains why these tests intentionally do not exercise retry logic and how they complement the unit tests. The test architecture properly separates concerns between HTTP-layer retry testing and ParseClient interface behavior.

packages/dart/test/src/network/parse_network_retry_test.dart (1)

1-10: Comprehensive unit test suite for retry mechanism.

The test file provides thorough coverage of the executeWithRetry function including success paths, retry scenarios, exception handling, timing, and configuration validation. The 100-retry limit validation and edge cases (empty intervals, byte responses) are well-tested.

packages/dart/lib/src/network/parse_dio_client.dart (2)

208-218: Good fix for the JSON escaping issue.

The _buildErrorJson helper properly uses jsonEncode to escape error messages, addressing the past review concern about malformed JSON when error messages contain quotes or special characters.


41-61: Retry wrapper correctly applied to GET method.

The executeWithRetry wrapper properly encapsulates the entire HTTP operation including error handling. The DioException catch block is correctly placed inside the operation closure, ensuring retry logic can re-execute the full operation.

packages/dart/lib/src/network/parse_network_retry.dart (1)

60-145: Core retry logic is well-implemented with proper bounds checking.

The retry loop correctly handles:

  • Initial attempt plus configured retries
  • Early return on success or non-retryable errors
  • Exception handling with retry support
  • Delay application between attempts
  • Maximum retry limit enforcement

The defensive StateError at line 141 is appropriate for catching potential logic errors.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/dart/lib/src/network/parse_network_retry.dart (1)

154-174: Consider clarifying the upstream conversion mechanism.

The documentation mentions HTML responses from proxies trigger retries, but the implementation only checks for statusCode == -1. This works correctly because ParseErrorResponse (upstream) converts HTML error pages to status -1, but this dependency isn't immediately obvious from the documentation.

Consider adding a brief note like: "Note: HTML responses are converted to status -1 by ParseErrorResponse before reaching this check."

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5096a9e and 26d407d.

📒 Files selected for processing (1)
  • packages/dart/lib/src/network/parse_network_retry.dart (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test Dart 3.10, Windows
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Test Flutter 3.38, Ubuntu
🔇 Additional comments (3)
packages/dart/lib/src/network/parse_network_retry.dart (3)

25-44: Idempotency warnings are well-documented.

The documentation properly warns about retry risks for non-idempotent POST/PUT operations and provides clear mitigation strategies. This addresses the previous review concerns about misleading idempotency claims.


67-102: Retry loop logic is correct.

The loop correctly handles:

  • Initial attempt plus configurable retries based on retryIntervals.length
  • Empty intervals list results in single attempt with no retries
  • Early exit on success or non-retryable responses

124-151: Good defensive programming with the StateError.

The exception handling correctly mirrors the success path logic, and the StateError at the end serves as a safety net to catch any future logic errors, even though it should be unreachable with the current implementation.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 6, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/dart/lib/parse_server_sdk.dart (1)

122-123: Consider adding parameter documentation.

The parameters are properly added and forwarded, maintaining backward compatibility. However, the doc comment for the initialize method (lines 92-103) doesn't mention the new retry configuration parameters. Consider adding brief documentation explaining these parameters and their defaults.

Example addition to the doc comment:

/// * [restRetryIntervals] - Optional list of retry delay intervals (in milliseconds) 
///   for read operations. Defaults to [0, 250, 500, 1000, 2000].
/// * [restRetryIntervalsForWrites] - Optional list of retry delay intervals for 
///   write operations. Defaults to [] (no retries) to prevent duplicate data.

Also applies to: 150-151

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26d407d and 8135d14.

📒 Files selected for processing (7)
  • packages/dart/lib/parse_server_sdk.dart (3 hunks)
  • packages/dart/lib/src/data/parse_core_data.dart (3 hunks)
  • packages/dart/lib/src/network/parse_dio_client.dart (6 hunks)
  • packages/dart/lib/src/network/parse_http_client.dart (7 hunks)
  • packages/dart/lib/src/network/parse_network_retry.dart (1 hunks)
  • packages/dart/test/src/network/parse_client_retry_integration_test.dart (1 hunks)
  • packages/dart/test/src/network/parse_network_retry_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/dart/lib/src/network/parse_http_client.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Dart 3.10, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter 3.38, macOS
🔇 Additional comments (21)
packages/dart/lib/src/data/parse_core_data.dart (3)

35-36: LGTM! Clean parameter additions.

The optional nullable parameters preserve backward compatibility while enabling configuration of retry behavior for both read and write operations.


57-60: Appropriate default retry configuration.

The defaults align well with the PR objectives:

  • Read operations (GET): 5 attempts with exponential backoff to handle transient network issues
  • Write operations (POST/PUT): No retries by default to prevent duplicate data creation

The exponential backoff intervals (0, 250, 500, 1000, 2000 ms) provide a good balance between quick recovery and avoiding server overload.


98-99: LGTM! Field declarations are appropriate.

The late modifier is correct since these fields are initialized in the init method. Non-final allows runtime reconfiguration if needed (as seen in test files).

packages/dart/test/src/network/parse_client_retry_integration_test.dart (3)

9-34: Excellent architectural documentation!

The documentation clearly explains why these integration tests bypass the retry mechanism and how they complement the unit tests. This will help future developers understand the testing strategy and avoid confusion about coverage metrics.


47-75: Well-structured integration test.

The test correctly verifies that HTML error responses are processed without retry when using a mocked client. The expectation of callCount == 1 accurately demonstrates the architectural decision that retry logic operates at the HTTP layer, not at the ParseClient interface.


181-208: Correctly validates non-retry behavior for Parse errors.

The test properly demonstrates that valid Parse Server error codes (like 101 ObjectNotFound) are not retried, which is the correct behavior since these are application-level errors that won't resolve with retries.

packages/dart/test/src/network/parse_network_retry_test.dart (4)

12-65: Comprehensive coverage of success paths.

These tests properly validate that successful responses (200, 201) and valid Parse Server errors (101) don't trigger unnecessary retries, confirming efficient operation.


198-218: Timing test with appropriate tolerance.

The test validates retry delay intervals but uses a relaxed threshold (>200ms for expected 300ms) to account for CI environment variability. The comment acknowledges this consideration, which is good practice for reliable CI execution.


220-234: Important safety validation test.

The max retry limit of 100 prevents accidental infinite retry loops. This test ensures the guard rail is properly enforced.


514-560: Critical write operation behavior properly tested.

These tests validate the important distinction between read and write retry behavior. The default of no retries for writes (line 531: callCount == 1) prevents accidental duplicate data creation, while still allowing custom configuration when needed.

packages/dart/lib/parse_server_sdk.dart (1)

39-39: LGTM! Proper part file inclusion.

The new part file is correctly placed with other network-related parts and follows the existing structure.

packages/dart/lib/src/network/parse_network_retry.dart (4)

3-81: Excellent comprehensive documentation.

The documentation thoroughly addresses retry behavior, idempotency concerns, and provides clear guidance on safe usage. The warning about POST/PUT non-idempotency and default no-retry behavior for writes demonstrates careful consideration of data integrity.


92-100: Good safety guard with clear error message.

The max retry limit of 100 prevents accidental configuration errors that could cause excessive retry attempts. The error message clearly explains the constraint and includes the actual length for debugging.


106-163: Retry loop logic is correct.

The loop properly handles:

  • Initial attempt + configured retries (loop from 0 to length, inclusive)
  • Early return on success (lines 113-120)
  • Final attempt return when retries exhausted (lines 124-130)
  • Exception handling with retry (lines 142-162)
  • Appropriate delays between attempts (line 141)

The debug logging provides useful insight into retry progress.


172-191: Simple and correct retry logic.

The _shouldRetryResponse function correctly identifies retriable responses by checking for status code -1 (ParseError.otherCause). The documentation mentions HTML responses, which makes sense because HTML parsing failures are converted to status -1 by the HTTP client layer before reaching this function.

packages/dart/lib/src/network/parse_dio_client.dart (6)

8-20: Helpful coverage documentation.

The documentation explaining the low coverage percentage and the architectural rationale is very useful. It prevents confusion about coverage metrics and clarifies that retry logic is tested elsewhere at 100%.


36-61: Clean retry integration for GET requests.

The implementation correctly wraps the GET operation with retry logic. GET operations are idempotent and safe to retry, so the default retry behavior (no isWriteOperation flag) is appropriate.


229-248: DELETE retry behavior is appropriate.

DELETE operations are not flagged with isWriteOperation: true, meaning they use the default read retry intervals. This is correct because:

  • DELETE operations are typically idempotent (deleting an already-deleted object usually succeeds or returns 404)
  • Transient network failures during DELETE are safe to retry
  • This aligns with the PR objectives (line 35 in parse_network_retry.dart: "DELETE operations: Retries enabled")

211-222: Well-implemented error JSON helper.

The _buildErrorJson helper properly addresses the JSON escaping concern from past reviews by using jsonEncode on a Map structure. This ensures error messages containing quotes or special characters won't produce malformed JSON.


165-202: Proper write operation handling with consistent error payloads.

The postBytes method correctly:

  • Marks the operation as a write (isWriteOperation: true)
  • Handles both DioException cases (with and without response)
  • Uses _buildErrorJson for consistent error formatting when response is unavailable

251-251: Consistent error payload for fallback case.

The _fallbackErrorData getter now returns a properly structured JSON error payload using _buildErrorJson, ensuring consistency across all error cases.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 7, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/dart/lib/parse_server_sdk.dart (1)

104-113: Clarify how retry configs apply across all HTTP methods

The parameter docs clearly separate read vs write retries and explain the defaults, which is good. Since the HTTP clients also expose head and patch methods (per the PR/AI summary), consider briefly noting which bucket those fall into (read-like vs write-like) so callers aren’t left guessing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8135d14 and 7e8623d.

📒 Files selected for processing (1)
  • packages/dart/lib/parse_server_sdk.dart (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test Flutter 3.38, Ubuntu, WASM
  • GitHub Check: Test Flutter 3.38, Windows
  • GitHub Check: Test Flutter beta
  • GitHub Check: Test Flutter 3.38, macOS
  • GitHub Check: Test Flutter 3.38, Ubuntu
🔇 Additional comments (3)
packages/dart/lib/parse_server_sdk.dart (3)

39-39: Wiring in parse_network_retry.dart as a library part looks correct

The new part placement is consistent with the existing network parts and cleanly exposes the shared retry implementation to the public SDK surface.


132-133: New retry parameters on Parse.initialize are backward compatible

Adding restRetryIntervals and restRetryIntervalsForWrites as nullable named parameters preserves existing call sites while cleanly surfacing retry configuration through the main initialization API. Naming and types align with their intended usage.


160-161: Retry configuration is correctly threaded into ParseCoreData.init

Passing both retry lists through to ParseCoreData.init keeps all behavior centralized in the core data layer. Please just double‑check that ParseCoreData.init applies the documented defaults ([0, 250, 500, 1000, 2000] for reads and [] for writes) when these are null, so the docs and behavior stay in sync.

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Impliment automatic retry on response error

1 participant