feat(fetch)!: integrate block-backed blob and stream-first multipart#6
feat(fetch)!: integrate block-backed blob and stream-first multipart#6
Conversation
BREAKING CHANGE: Blob now implements block.Block and removes synchronous copyBytes(). Blob.slice follows Web Blob negative-index semantics. FormData.encodeMultipart now returns stream-first MultipartBody and MultipartBody.bytes is async bytes().
📝 WalkthroughWalkthroughThis PR reimplements Blob as a block-backed type that implements block.Block, converts FormData multipart encoding to a stream-first MultipartBody with async bytes(), updates Blob.slice to follow Web Blob negative-index semantics, and adds BodyInit support for package:block Block in Request/Response. The package version and dependency were updated. Changes
Sequence Diagram(s)sequenceDiagram
actor Caller
participant FormData
participant MultipartBody
participant Stream as Stream<Uint8List>
participant Bytes
Caller->>FormData: encodeMultipart(boundary)
activate FormData
FormData->>MultipartBody: create via factory (streamFactory, boundary, contentLength)
FormData-->>Caller: return MultipartBody
deactivate FormData
Caller->>MultipartBody: stream (getter)
activate MultipartBody
MultipartBody->>Stream: invoke streamFactory()
Stream-->>MultipartBody: Stream<Uint8List>
MultipartBody-->>Caller: return Stream<Uint8List>
deactivate MultipartBody
alt Materialize bytes
Caller->>MultipartBody: bytes()
activate MultipartBody
MultipartBody->>Stream: collect chunks
Stream->>Bytes: gather
Bytes-->>MultipartBody: return Uint8List
MultipartBody-->>Caller: Uint8List
deactivate MultipartBody
end
sequenceDiagram
actor Caller
participant Request
participant BodyData
participant Block as block.Block
participant Stream as Stream<Uint8List>
Caller->>Request: new(body: blockBody)
activate Request
Request->>BodyData: fromInit(blockBody)
activate BodyData
BodyData->>Block: blockBody.stream()
Block-->>BodyData: Stream<Uint8List>
BodyData->>BodyData: defaultContentType <- blockBody.type
BodyData->>BodyData: defaultContentLength <- blockBody.size
BodyData-->>Request: BodyData(stream, contentType, contentLength)
deactivate BodyData
Request-->>Caller: Request instance
Caller->>Request: text()
Request->>Stream: consume
Stream-->>Caller: chunks -> decoded text
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@pubspec.yaml`:
- Line 3: The pubspec dependency constraint for the package named "block" is
invalid (there is no 1.0.0 release); update the dependency entry for block in
pubspec.yaml from ^1.0.0 to ^0.0.3 (or the intended actual released version) so
pub get succeeds—locate the "block:" dependency line and replace the version
constraint accordingly.
🧹 Nitpick comments (4)
test/request_response_test.dart (1)
192-199: Good test for block body in Response — mirrors the Request test for completeness.Consider also adding a test for
Blob.slicewith negative indices on a non-inline (block-backed) Blob to ensure the block path inslice()handles negative indices consistently. This ties into the concern flagged inblob.dartabout potential inconsistency between the inline and block paths.lib/src/fetch/form_data.dart (2)
89-91: Consider documenting or deprecatingencodeMultipartStream.Since this is now a pure alias for
encodeMultipart, callers might be confused by two identical methods. If it exists only for backward compatibility, a@Deprecatedannotation would signal intent and guide migration.
175-210:_calculateMultipartLengthduplicates the structural logic of_encodeMultipart— divergence risk.The header templates, boundary markers, and CRLF sequences are mirrored in both methods. If one is updated without the other,
contentLengthwill silently diverge from the actual stream payload, causing subtle HTTP framing bugs for clients relying onContent-Length.Consider extracting a shared representation of the part structure (e.g., a list of part descriptors) that both methods consume, or a single traversal that accepts a callback/visitor to either yield chunks or sum lengths.
test/form_data_test.dart (1)
73-95: Key assertion:contentLengthvs actual bytes length (line 89).This test is the primary safety net verifying that
_calculateMultipartLengthstays in sync with_encodeMultipart. Good that it's present — consider also adding a case with multi-byte UTF-8 characters (e.g., emoji or CJK) in field names/values to stress the length calculation.
| name: ht | ||
| description: Fetch-style HTTP types and protocol abstractions for Dart. | ||
| version: 0.1.0 | ||
| version: 0.2.0 |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
pub.dev block dart package 1.0.0
💡 Result:
There is a Dart package named block on pub.dev, but it does not have a 1.0.0 release. The highest published stable version currently listed is 0.0.3. [1]
To use it, add (example):
dependencies:
block: ^0.0.3Package docs (API/usage) are here: [2]
Sources:
[1] pub.dev “block – All Versions” (pub.dev)
[2] pub.dev “block – Dart API docs” (pub.dev)
Citations:
- 1: https://pub.dev/packages/block/versions?utm_source=openai
- 2: https://pub.dev/documentation/block/latest/?utm_source=openai
🏁 Script executed:
# Find and examine pubspec.yaml
git ls-files pubspec.yamlRepository: medz/ht
Length of output: 63
🏁 Script executed:
# Check the current state of pubspec.yaml
cat -n pubspec.yaml | head -30Repository: medz/ht
Length of output: 705
🏁 Script executed:
# Check git diff to see what changed in pubspec.yaml
git diff HEAD~1 pubspec.yaml 2>/dev/null || git show HEAD:pubspec.yaml 2>/dev/null | head -30Repository: medz/ht
Length of output: 530
Fix block dependency version: 1.0.0 does not exist.
The block package on pub.dev does not have a 1.0.0 release; the highest stable version is 0.0.3. Change block: ^1.0.0 to block: ^0.0.3 (or the intended version if a newer release is planned). The current constraint will fail during pub get.
🤖 Prompt for AI Agents
In `@pubspec.yaml` at line 3, The pubspec dependency constraint for the package
named "block" is invalid (there is no 1.0.0 release); update the dependency
entry for block in pubspec.yaml from ^1.0.0 to ^0.0.3 (or the intended actual
released version) so pub get succeeds—locate the "block:" dependency line and
replace the version constraint accordingly.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@lib/src/fetch/blob.dart`:
- Around line 89-91: The ByteBuffer branch in the blob normalization creates a
non-owning view (ByteData.sublistView(part.asUint8List())) which can be mutated
by callers; change the logic in the function handling ByteBuffer (the branch
that checks `if (part is ByteBuffer)`) to first make a defensive copy (e.g.,
create a new Uint8List via `Uint8List.fromList(part.asUint8List())` or clone the
buffer) and then return a ByteData view over that copied buffer so it behaves
consistently with the `Uint8List.fromList()` copies used in the other branches.
🧹 Nitpick comments (1)
lib/src/fetch/blob.dart (1)
42-47: Circular delegation betweenbytes()andarrayBuffer()is correct but worth noting.
bytes()calls_inner.arrayBuffer(), andarrayBuffer()callsbytes(). Sincebytes()reaches into_inner(notthis.arrayBuffer()), there is no infinite recursion — just a double-copy if someone callsarrayBuffer()→bytes()→Uint8List.fromList(await _inner.arrayBuffer()). This is fine given the doc says "returns a copy", but if you ever want to avoid the copy forarrayBuffer(), you could delegatearrayBuffer()directly to_inner.arrayBuffer()as well.
lib/src/fetch/blob.dart
Outdated
| if (part is ByteBuffer) { | ||
| return ByteData.sublistView(part.asUint8List()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/src/fetch/blob.dart | head -100 | tail -30Repository: medz/ht
Length of output: 884
🏁 Script executed:
# Find the block.Block class
fd -t f 'block\.dart' --type fRepository: medz/ht
Length of output: 33
🏁 Script executed:
# Search for block.Block class definition
ast-grep --pattern 'class Block {
$$$
}'Repository: medz/ht
Length of output: 33
🏁 Script executed:
fd 'block' -t f | grep -E '\.(dart|js)$'Repository: medz/ht
Length of output: 33
🏁 Script executed:
# Search for Block class definition in Dart files
rg 'class Block' -t dart -A 10Repository: medz/ht
Length of output: 33
🏁 Script executed:
# Also check the import statements in blob.dart to understand where block comes from
head -30 lib/src/fetch/blob.dartRepository: medz/ht
Length of output: 971
🏁 Script executed:
# Check pubspec.yaml for block package and its source
cat pubspec.yaml | grep -A 5 -B 5 blockRepository: medz/ht
Length of output: 198
🏁 Script executed:
# Check if block is a local package
ls -la | grep -i blockRepository: medz/ht
Length of output: 33
🏁 Script executed:
# Search for any Block class definition or implementation in the repo
rg 'class Block' -t dart -B 2 -A 15Repository: medz/ht
Length of output: 33
🌐 Web query:
Dart block package pub.dev version 1.0.0
💡 Result:
On pub.dev, the block package (publisher medz.dev) does not have a 1.0.0 release. As of February 6, 2026, the available stable versions are 0.0.3 (latest), 0.0.2, 0.0.1, and 0.0.0. [1]
If you meant the popular state-management package bloc (no “k”), tell me and I’ll look up its 1.0.0 release.
Sources: [1]
🌐 Web query:
Dart block package ByteData sublistView defensive copy
💡 Result:
ByteData.sublistView(...) is not a defensive copy. It creates a view into the same underlying ByteBuffer (zero-copy), so mutations are shared through the buffer. (api.dart.dev)
If you need a defensive copy, copy the bytes into a new buffer:
import 'dart:typed_data';
// Defensive copy of a ByteData range:
ByteData copyByteDataRange(TypedData data, int start, int end) {
final view = Uint8List.sublistView(data, start, end); // view (no copy)
final bytes = Uint8List.fromList(view); // copy
return ByteData.sublistView(bytes); // view on the copied buffer
}Why this matters for package:block: the package explicitly optimizes for slicing without copying / sharing backing storage (e.g., slice() can share a backing file/view for larger slices), so you should not assume slices or stored parts are isolated unless you copy them yourself. (pub.dev)
Citations:
- 1: https://api.dart.dev/dart-typed_data/ByteData/ByteData.sublistView.html?utm_source=openai
- 2: https://pub.dev/packages/block
ByteBuffer normalization creates a view rather than a defensive copy, unlike Uint8List and List<int>.
Line 90 wraps the buffer in a ByteData view without copying, while lines 94 and 98 copy their inputs with Uint8List.fromList(). Since the block package explicitly optimizes for storage sharing without defensive copying, a caller who mutates the original ByteBuffer after constructing the Blob could corrupt blob contents. For consistency and safety, make a copy:
Proposed fix
if (part is ByteBuffer) {
- return ByteData.sublistView(part.asUint8List());
+ return Uint8List.fromList(part.asUint8List());
}📝 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.
| if (part is ByteBuffer) { | |
| return ByteData.sublistView(part.asUint8List()); | |
| } | |
| if (part is ByteBuffer) { | |
| return Uint8List.fromList(part.asUint8List()); | |
| } |
🤖 Prompt for AI Agents
In `@lib/src/fetch/blob.dart` around lines 89 - 91, The ByteBuffer branch in the
blob normalization creates a non-owning view
(ByteData.sublistView(part.asUint8List())) which can be mutated by callers;
change the logic in the function handling ByteBuffer (the branch that checks `if
(part is ByteBuffer)`) to first make a defensive copy (e.g., create a new
Uint8List via `Uint8List.fromList(part.asUint8List())` or clone the buffer) and
then return a ByteData view over that copied buffer so it behaves consistently
with the `Uint8List.fromList()` copies used in the other branches.
Summary
package:blockintohtbinary primitives.Blobdirectly compatible withblock.Block.FormData.encodeMultipart()as the primary API and switch multipart payloads to stream-firstMultipartBody.BodyInitsupport forblock.Blockvalues in bothRequestandResponse.Breaking Changes
Blob.copyBytes()is removed.Blob.slicenow follows Web Blob semantics for negative indices.MultipartBody.bytesfield is replaced by async methodbytes().FormData.encodeMultipart()now returns a stream-first payload (stream,contentLength,contentType,bytes()).Migration
Blob copy
Multipart bytes
Performance (local benchmark)
Measured on local machine, comparing
mainvsfeat/block-stream-refactor(3-run average).Decision
This PR intentionally keeps
Blobas a thinBlock-based adapter.We do not add extra fast-path/caching optimizations in this PR to avoid higher implementation complexity and potential memory overhead.
Validation
dart format --output=none --set-exit-if-changed .dart analyzedart testdart run example/main.dart