Skip to content

Conversation

@GarethIW
Copy link
Member

@GarethIW GarethIW commented Jan 15, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced file upload reliability with automatic retry logic and exponential backoff between attempts.
    • Improved error handling to distinguish authentication failures from other network errors.
    • Added request timeout handling to prevent indefinite hangs.
    • Enhanced diagnostic logging for upload troubleshooting.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

The change replaces UnityWebRequest-based file upload with an asynchronous HttpClient implementation that includes retry logic with exponential backoff, improved error handling for authentication/authorization failures, and per-request cancellation timeouts.

Changes

Cohort / File(s) Summary
HTTP Client Upload Refactor
Editor/Resources/Builder/SideQuest/SqEditorAppApi.cs
Introduces shared HttpClient with custom handler (decompression disabled, 10-minute global timeout). Replaces UnityWebRequest upload flow with async implementation: UploadFileWithRetryAsync handles retries with exponential backoff (2, 4, 8s), per-attempt logging, and timeout management. Enhanced error handling: 401/403 → SqEditorApiAuthException; other HTTP errors → SqEditorApiException; exhausted retries → SqEditorApiNetworkException. Updates UploadFileInternal coroutine to bridge async/await and propagate exceptions via OnError.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant UploadFileInternal as UploadFileInternal<br/>(Coroutine)
    participant RetryAsync as UploadFileWithRetryAsync<br/>(Async)
    participant HttpClient
    participant ErrorHandler

    Caller->>UploadFileInternal: Initiate upload
    UploadFileInternal->>RetryAsync: Start async task
    
    loop Retry Loop (max 3 attempts)
        RetryAsync->>HttpClient: Send POST request
        alt HTTP 2xx
            HttpClient-->>RetryAsync: Success (status code)
            RetryAsync-->>UploadFileInternal: Return status
        else HTTP 401/403
            HttpClient-->>ErrorHandler: Auth error response
            ErrorHandler-->>RetryAsync: SqEditorApiAuthException
            RetryAsync-->>UploadFileInternal: Throw exception
        else HTTP other error
            HttpClient-->>ErrorHandler: Error response
            ErrorHandler-->>RetryAsync: SqEditorApiException
            RetryAsync-->>UploadFileInternal: Throw exception
        else Timeout
            ErrorHandler-->>RetryAsync: Cancellation timeout
            RetryAsync->>RetryAsync: Exponential backoff (2/4/8s)
        end
    end
    
    UploadFileInternal->>Caller: Complete or OnError callback
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hopping through uploads with async delight,
HttpClient whispers where UnityWeb used to sight,
Retries bounce back with exponential grace,
Error handling catches each troublesome case,
A fluffy refactor of network embrace! 🌐✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Re-implement cdn file upload with HttpClient' directly and accurately describes the main change: replacing UnityWebRequest-based upload with HttpClient-based implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

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

🧹 Nitpick comments (2)
Editor/Resources/Builder/SideQuest/SqEditorAppApi.cs (2)

503-517: Add IsCanceled check for defensive completeness.

The coroutine bridge only checks IsFaulted, but tasks can also be in a Canceled state. While UploadFileWithRetryAsync catches TaskCanceledException internally, adding this check improves robustness against future changes.

Suggested improvement
 // Check for exceptions
-if (uploadTask.IsFaulted)
+if (uploadTask.IsFaulted || uploadTask.IsCanceled)
 {
     var ex = uploadTask.Exception?.InnerException ?? uploadTask.Exception;
+    if (ex == null && uploadTask.IsCanceled)
+    {
+        ex = new OperationCanceledException("Upload was canceled");
+    }
     OnError?.Invoke(ex);
     yield break;
 }

532-535: Set an explicit Content-Type header for the upload request.

The ByteArrayContent is created without an explicit Content-Type header. Following the pattern used elsewhere in the codebase (e.g., SqEditorAppApi.cs:747, 811), explicitly set the header using content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); or the appropriate MIME type for the uploaded file to ensure compatibility with the CDN/storage service.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be38943 and 44e873c.

📒 Files selected for processing (1)
  • Editor/Resources/Builder/SideQuest/SqEditorAppApi.cs
🔇 Additional comments (2)
Editor/Resources/Builder/SideQuest/SqEditorAppApi.cs (2)

46-54: LGTM - Good use of static HttpClient.

Static HttpClient is the recommended pattern for connection pooling and avoiding socket exhaustion. The configuration is appropriate for file upload scenarios.


584-590: Exponential backoff implementation looks correct.

The backoff calculation Math.Pow(2, attempt) yields 2s, 4s, 8s for attempts 1, 2, 3 respectively, which is a reasonable exponential backoff strategy.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +543 to +556
if (!response.IsSuccessStatusCode)
{
var responseBody = await response.Content.ReadAsStringAsync();

if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
response.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
throw new SqEditorApiAuthException((int)response.StatusCode,
$"HTTP {response.StatusCode}: {response.ReasonPhrase} - {responseBody}");
}

throw new SqEditorApiException(
$"HTTP {response.StatusCode}: {response.ReasonPhrase} - {responseBody}");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Non-auth HTTP errors (4xx/5xx) will be retried, which is often undesirable for client errors.

When SqEditorApiException is thrown for non-auth HTTP errors, it's caught by the generic Exception handler at line 578 and retried. Client errors like 400 Bad Request or 404 Not Found won't succeed on retry and will waste time/resources.

Consider either:

  1. Not retrying SqEditorApiException (similar to auth exceptions), or
  2. Only retrying for 5xx server errors
Suggested fix - Don't retry SqEditorApiException
 catch (SqEditorApiAuthException)
 {
     // Don't retry auth exceptions
     throw;
 }
+catch (SqEditorApiException)
+{
+    // Don't retry HTTP errors (4xx, 5xx) - these are server responses, not transient failures
+    throw;
+}
 catch (Exception ex)
 {
     lastException = ex;
     LogLine.Do($"Upload failed with exception: {ex.Message}");
 }

@GarethIW GarethIW merged commit e581d9d into main Jan 15, 2026
1 of 2 checks passed
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.

3 participants