Skip to content

fix(chunk upload): update chunk upload logic to include data key - #73

Merged
atticusofsparta merged 8 commits into
masterfrom
fix-chunked-uploads
Mar 19, 2026
Merged

atticusofsparta merged 8 commits into
masterfrom
fix-chunked-uploads

Conversation

@atticusofsparta

@atticusofsparta atticusofsparta commented Mar 4, 2026 •

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Exponential backoff and retry controls for chunked uploads to improve robustness.
    • Smarter detection of retriable propagation errors so retries are applied more selectively and messages reflect backoff status.
  • Bug Fixes

    • Transaction POST behavior corrected so header/body data is handled consistently for chunked vs in-body uploads.
  • Tests

    • Added tests covering single-chunk and multi-chunk upload flows and POST payloads.

@coderabbitai

coderabbitai Bot commented Mar 4, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds exponential-backoff controls for chunk uploads, propagation-aware handling of non-200 chunk responses, posts transaction headers with an explicit empty data when uploading chunks, adds a helper to detect retriable propagation responses, and expands tests for chunked vs single-chunk /tx payloads.

Changes

Cohort / File(s) Summary
Uploader implementation
lib/src/models/transaction_uploader.dart, lib/src/streams/transaction_uploader.dart
Introduce private retry/backoff constants and apply retry(...) to chunk upload calls; enhance non-200 response handling to detect propagation-retriable responses and surface distinct retriable vs fatal errors; ensure header POST includes txJson['data'] = '' for chunked uploads and set Content-Type appropriately.
Chunk propagation helper
lib/src/utils.dart
Add isChunkPropagationRetriableResponse(int statusCode, String body) to parse 400 multi-peer responses and determine if propagation failures should trigger backoff retries.
API client
lib/src/api/api.dart
Change POST behavior to omit a body when body is null; include Content-Type and JSON body only when body is non-null.
Tests
test/transaction_uploader_test.dart
Add tests (browser-skipped) asserting: chunked uploads POST /tx with data: "" and use /chunk requests for payloads; single-chunk uploads POST base64 data in /tx; validate chunk POST fields and counts via MockClient.
Control-flow / messages
(implicit across changed files)
Propagate propagation-retriable status into retry contexts and adjust thrown error messages to distinguish retriable backoff vs fatal failures.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TransactionUploader
    participant ChunkUploader
    participant API

    Client->>TransactionUploader: create & sign transaction (with data)
    TransactionUploader->>ChunkUploader: determine uploadInBody?
    alt Single-chunk
        ChunkUploader->>API: POST /tx (Content-Type: application/json, data: base64)
        API-->>ChunkUploader: 200 OK
        ChunkUploader-->>TransactionUploader: success
    else Chunked
        TransactionUploader->>API: POST /tx (Content-Type: application/json, data: "")
        API-->>TransactionUploader: 200 OK
        TransactionUploader->>ChunkUploader: start parallel chunk uploads
        loop per chunk
            ChunkUploader->>API: POST /chunk (Content-Type: application/json, chunk payload)
            alt 200
                API-->>ChunkUploader: 200 OK
            else non-200
                ChunkUploader->>ChunkUploader: read bodyString & isChunkPropagationRetriableResponse(status, bodyString)
                alt retriable
                    ChunkUploader->>ChunkUploader: throw retriable error → retry with backoff
                else fatal
                    ChunkUploader-->>TransactionUploader: fatal error
                end
            end
        end
        ChunkUploader-->>TransactionUploader: completion notify
    end
    TransactionUploader-->>Client: final result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • arielmelendez

Poem

🐰 I nibble bytes in midnight code,

Empty fields where chunks will go,
I hop with backoff, patient, spry,
Retry, propagate, then try—
A carrot patch beneath the sky. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: updating chunk upload logic to include a data key in transaction header posting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-chunked-uploads
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/transaction_uploader_test.dart (1)

15-69: Harden chunked-path test by asserting /chunk is actually used.

Right now the test validates /tx body shape, but it stops after the first event (header post). Add a /chunk call counter and assert it’s > 0 to lock in the full chunked behavior.

♻️ Suggested patch
-      String? capturedTxBody;
+      String? capturedTxBody;
+      var chunkPostCount = 0;
       final mockClient = MockClient((request) async {
@@
           if (request.url.path.endsWith('chunk')) {
+            chunkPostCount++;
             return http.Response('', 200);
           }
@@
-      final events = uploader.upload().take(1);
+      final events = uploader.upload().take(2);
       await for (final _ in events) {
-        break; // Consume first event (after header is posted).
+        break; // Consume through first chunk completion.
       }
@@
       expect(txJson['data'], equals(''),
           reason: 'Chunked upload must send empty data; payload goes via /chunk');
+      expect(chunkPostCount, greaterThan(0),
+          reason: 'Chunked upload must send payload chunks to /chunk');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/transaction_uploader_test.dart` around lines 15 - 69, The test currently
stops after consuming the first uploader event and never asserts that the /chunk
endpoint was actually hit; modify the MockClient handler to increment a counter
(e.g., chunkPostCount) whenever a POST request.url.path.endsWith('chunk') is
handled, then drive the uploader to complete (consume all events from
uploader.upload() instead of breaking after the first event) and add an
expectation that chunkPostCount > 0; update references in this test
(capturedTxBody, mockClient, uploader.upload()) to use the new counter and final
assertion to ensure the chunked upload path was exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/transaction_uploader_test.dart`:
- Around line 15-69: The test currently stops after consuming the first uploader
event and never asserts that the /chunk endpoint was actually hit; modify the
MockClient handler to increment a counter (e.g., chunkPostCount) whenever a POST
request.url.path.endsWith('chunk') is handled, then drive the uploader to
complete (consume all events from uploader.upload() instead of breaking after
the first event) and add an expectation that chunkPostCount > 0; update
references in this test (capturedTxBody, mockClient, uploader.upload()) to use
the new counter and final assertion to ensure the chunked upload path was
exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb16147e-56e1-4db0-84e1-9da0066d5328

📥 Commits

Reviewing files that changed from the base of the PR and between 60562fb and e94f354.

📒 Files selected for processing (2)
  • lib/src/models/transaction_uploader.dart
  • test/transaction_uploader_test.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
lib/src/streams/transaction_uploader.dart (1)

234-258: ⚠️ Potential issue | 🟠 Major

This non-200 classification path never executes with Dio's default configuration.

The Dio() instance at line 230 has no configuration override. By default, Dio throws DioException for any status code outside 2xx—this exception is caught by the retry() wrapper's onRetry handler before execution reaches the res.statusCode != 200 block. The fatal error classification (_fatalChunkUploadErrors) and propagation-retriable response detection logic are therefore unreachable.

To inspect responses with non-2xx status codes, either set validateStatus to accept those codes or catch DioException and examine exception.response before deciding whether to retry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/streams/transaction_uploader.dart` around lines 234 - 258, The
non-200 handling in the chunk upload never runs because Dio() throws
DioException for non-2xx by default; update the upload logic in
transaction_uploader.dart to either configure the Dio instance passed to the
POST call with a validateStatus that accepts non-2xx responses OR wrap the await
dio.post(...) in a try/catch for DioException and inspect exception.response
(then call getResponseErrorFromDioRespose, compute bodyString and
isChunkPropagationRetriableResponse, and consult _fatalChunkUploadErrors) before
rethrowing or converting to the same error types you currently produce; ensure
this integrates with the existing retry() wrapper so the onRetry semantics still
apply.
🧹 Nitpick comments (1)
lib/src/models/transaction_uploader.dart (1)

149-168: Avoid serializing the full payload before overwriting data.

_transaction.toJson() already base64-encodes data, and the chunked path immediately replaces that field with ''. On large uploads this does a full payload allocation only to discard it; a header-only serializer would keep this fix without the extra memory churn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/models/transaction_uploader.dart` around lines 149 - 168, The code
calls _transaction.toJson() (which base64-encodes the full data) and then
immediately replaces txJson['data'] = '' on the chunked path, causing
unnecessary large allocations; change the logic to avoid serializing the full
payload when uploadInBody is false by either adding an option to
Transaction.toJson({bool includeData = true}) and calling
_transaction.toJson(includeData: false) or implement a lightweight method like
_transaction.toHeaderJson() that returns a map without the base64 data, and use
that result for the chunked-path branch (keep the existing txJson['data'] = ''
behavior for node compatibility).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/utils.dart`:
- Around line 107-109: The helper currently treats responses as retriable unless
successCount is an int > 0; instead require successCount == 0 to mark retriable.
Locate the block that reads final successCount = map['successCount']; and change
the logic so that the function returns true only when successCount is an int and
equals 0, and returns false for any missing or non-integer successCount (and for
any successCount > 0). Ensure the updated condition replaces the current if
(successCount is int && successCount > 0) return false; return true; behavior.

---

Outside diff comments:
In `@lib/src/streams/transaction_uploader.dart`:
- Around line 234-258: The non-200 handling in the chunk upload never runs
because Dio() throws DioException for non-2xx by default; update the upload
logic in transaction_uploader.dart to either configure the Dio instance passed
to the POST call with a validateStatus that accepts non-2xx responses OR wrap
the await dio.post(...) in a try/catch for DioException and inspect
exception.response (then call getResponseErrorFromDioRespose, compute bodyString
and isChunkPropagationRetriableResponse, and consult _fatalChunkUploadErrors)
before rethrowing or converting to the same error types you currently produce;
ensure this integrates with the existing retry() wrapper so the onRetry
semantics still apply.

---

Nitpick comments:
In `@lib/src/models/transaction_uploader.dart`:
- Around line 149-168: The code calls _transaction.toJson() (which
base64-encodes the full data) and then immediately replaces txJson['data'] = ''
on the chunked path, causing unnecessary large allocations; change the logic to
avoid serializing the full payload when uploadInBody is false by either adding
an option to Transaction.toJson({bool includeData = true}) and calling
_transaction.toJson(includeData: false) or implement a lightweight method like
_transaction.toHeaderJson() that returns a map without the base64 data, and use
that result for the chunked-path branch (keep the existing txJson['data'] = ''
behavior for node compatibility).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9de35f0c-cf29-462c-ac72-3576010b2e2c

📥 Commits

Reviewing files that changed from the base of the PR and between e94f354 and d9ede26.

📒 Files selected for processing (3)
  • lib/src/models/transaction_uploader.dart
  • lib/src/streams/transaction_uploader.dart
  • lib/src/utils.dart

Comment thread lib/src/utils.dart
Comment on lines +107 to +109
final successCount = map['successCount'];
if (successCount is int && successCount > 0) return false;
return true;

@coderabbitai coderabbitai Bot Mar 6, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Require successCount == 0 before marking the response retriable.

If successCount is missing or non-integer, this helper currently falls through to true, so any 400 payload with a non-empty results list gets treated as propagation-retriable. That can stretch unrelated validation responses into the full backoff loop.

Proposed fix
   final successCount = map['successCount'];
-  if (successCount is int && successCount > 0) return false;
+  if (successCount is! int || successCount != 0) return false;
   return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final successCount = map['successCount'];
if (successCount is int && successCount > 0) return false;
return true;
final successCount = map['successCount'];
if (successCount is! int || successCount != 0) return false;
return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/utils.dart` around lines 107 - 109, The helper currently treats
responses as retriable unless successCount is an int > 0; instead require
successCount == 0 to mark retriable. Locate the block that reads final
successCount = map['successCount']; and change the logic so that the function
returns true only when successCount is an int and equals 0, and returns false
for any missing or non-integer successCount (and for any successCount > 0).
Ensure the updated condition replaces the current if (successCount is int &&
successCount > 0) return false; return true; behavior.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thats a fine behavior for other general 400s for now

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@atticusofsparta, understood! The intentional fallthrough behavior — treating any 400 with a non-empty results list as propagation-retriable (even when successCount is missing or non-integer) — is acceptable for now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/streams/transaction_uploader.dart`:
- Around line 52-58: The Dio POST calls (the dio.post(...) that posts
json.encode(headers) and the other POST used for chunk propagation) currently
throw DioException on non-2xx responses and never let you inspect
res.statusCode/res.data; update both calls to pass an Options with
validateStatus: (_) => true (added alongside contentType in the Options passed
to dio.post) so non-2xx responses are returned for inspection, keeping existing
error handling for network/timeouts intact; locate the calls by the symbols
dio.post, endpoint, data: json.encode(headers) and the companion
chunk-propagation POST and add validateStatus to their Options objects.
- Around line 28-32: Create a dedicated retriable exception type (e.g.,
RetriableChunkException) and change _uploadChunk() to throw that type only for
propagation/transient failures (the cases that should back off), while
continuing to throw plain Exception or specific non-retriable errors for
permanent 4xx/other failures; then update the retry(...) invocations that wrap
chunk uploads to pass retryIf: (e) => e is RetriableChunkException so the
exponential backoff
(_chunkRetryDelayFactor/_chunkRetryMaxDelay/_chunkRetryMaxAttempts) only applies
to propagation-related transient errors — apply the same change to the other
chunk-upload retry usages in this file that currently retry all Exceptions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 14c36ab4-6305-4a4c-880a-2750ce3d1a98

📥 Commits

Reviewing files that changed from the base of the PR and between d9ede26 and e019984.

📒 Files selected for processing (2)
  • lib/src/api/api.dart
  • lib/src/streams/transaction_uploader.dart

Comment thread lib/src/streams/transaction_uploader.dart
Comment thread lib/src/streams/transaction_uploader.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/transaction_uploader_test.dart (1)

62-64: Minor: Clarify or correct the comment about upload events.

The comment states "First event: header posted; second: at least one chunk completed" but _postChunks stream yields (uploadedChunks, totalChunks) tuples only for chunk completions, not for header posting. The header is posted before the stream begins yielding. Consider updating the comment to reflect the actual event semantics:

-      // First event: header posted; second: at least one chunk completed (POST /chunk).
+      // Each event represents a chunk completion; header is posted before stream begins.
       await uploader.upload().take(2).toList();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/transaction_uploader_test.dart` around lines 62 - 64, Comment is
inaccurate: update the test comment near client.transactions.getUploader and
uploader.upload to state that the header is posted before uploader.upload()
begins yielding and that the stream yields only chunk-completion tuples from
_postChunks (uploadedChunks, totalChunks); replace the current "First event:
header posted; second: at least one chunk completed" with a note like "header is
posted before the stream; stream emits (uploadedChunks, totalChunks) for chunk
completions" to accurately describe uploader.upload() semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/transaction_uploader_test.dart`:
- Around line 62-64: Comment is inaccurate: update the test comment near
client.transactions.getUploader and uploader.upload to state that the header is
posted before uploader.upload() begins yielding and that the stream yields only
chunk-completion tuples from _postChunks (uploadedChunks, totalChunks); replace
the current "First event: header posted; second: at least one chunk completed"
with a note like "header is posted before the stream; stream emits
(uploadedChunks, totalChunks) for chunk completions" to accurately describe
uploader.upload() semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 878454a0-25de-4ac0-bece-62d95b641ae9

📥 Commits

Reviewing files that changed from the base of the PR and between e019984 and 2d9b8f9.

📒 Files selected for processing (2)
  • lib/src/streams/transaction_uploader.dart
  • test/transaction_uploader_test.dart

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/transaction_uploader_test.dart (2)

91-143: LGTM! Clean test for single-chunk upload path.

The test correctly verifies that single-chunk transactions include base64-encoded data in the POST /tx body.

Consider adding an assertion to explicitly verify no POST /chunk requests occur, which would make the test more robust against regressions:

💡 Optional: Assert no chunk POST occurs
       String? capturedTxBody;
       http.Request? capturedTxRequest;
+      var chunkPostCount = 0;
       final mockClient = MockClient((request) async {
         if (request.method == 'GET') {
           ...
         }
         if (request.method == 'POST' && request.url.path.endsWith('tx')) {
           capturedTxRequest = request;
           capturedTxBody = request.body;
           return http.Response('', 200);
         }
+        if (request.method == 'POST' && request.url.path.endsWith('chunk')) {
+          chunkPostCount++;
+          return http.Response('', 200);
+        }
         return http.Response('', 404);
       });
       ...
       expect((txJson['data'] as String).length, greaterThan(0),
           reason: 'Single-chunk upload must send base64 data in body');
+      expect(chunkPostCount, equals(0),
+          reason: 'Single-chunk upload should not POST to /chunk');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/transaction_uploader_test.dart` around lines 91 - 143, Add an assertion
that no POST /chunk occurred by capturing any chunk requests in the same test
("single-chunk upload sends transaction JSON with data key set to base64
payload"); introduce a new variable (e.g., capturedChunkRequest) inside the
MockClient handler and set it when request.method == 'POST' &&
request.url.path.contains('chunk'), then after await
client.transactions.upload(transaction).drain() assert capturedChunkRequest is
null (similar to the existing checks for capturedTxRequest/capturedTxBody) so
the test fails if a chunk upload is attempted.

5-5: Use public API import for ArweaveSigner.

Replace import 'package:arweave/src/signer.dart'; with import 'package:arweave/arweave.dart'; (or import 'package:arweave';). The signer module is re-exported from the main package barrel file, and importing from the public API ensures better forward compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/transaction_uploader_test.dart` at line 5, Replace the direct private
import of the signer implementation with the public package barrel export:
change the import of 'package:arweave/src/signer.dart' to the public API import
(e.g., 'package:arweave/arweave.dart' or 'package:arweave') so tests use the
re-exported ArweaveSigner from the package's public API; update the import
statement where ArweaveSigner is referenced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/transaction_uploader_test.dart`:
- Around line 91-143: Add an assertion that no POST /chunk occurred by capturing
any chunk requests in the same test ("single-chunk upload sends transaction JSON
with data key set to base64 payload"); introduce a new variable (e.g.,
capturedChunkRequest) inside the MockClient handler and set it when
request.method == 'POST' && request.url.path.contains('chunk'), then after await
client.transactions.upload(transaction).drain() assert capturedChunkRequest is
null (similar to the existing checks for capturedTxRequest/capturedTxBody) so
the test fails if a chunk upload is attempted.
- Line 5: Replace the direct private import of the signer implementation with
the public package barrel export: change the import of
'package:arweave/src/signer.dart' to the public API import (e.g.,
'package:arweave/arweave.dart' or 'package:arweave') so tests use the
re-exported ArweaveSigner from the package's public API; update the import
statement where ArweaveSigner is referenced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d85fbc83-6c6d-422f-9e4e-492cc06a7328

📥 Commits

Reviewing files that changed from the base of the PR and between 2d9b8f9 and be0722e.

📒 Files selected for processing (1)
  • test/transaction_uploader_test.dart

@atticusofsparta
atticusofsparta merged commit 0a55ea0 into master Mar 19, 2026
4 checks passed
@atticusofsparta
atticusofsparta deleted the fix-chunked-uploads branch March 19, 2026 20:02
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.

2 participants