From e9181880b91bea7a717858ce6bd881d8685ef01a Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:19:52 +0800 Subject: [PATCH] feat(fetch): expose Body size --- CHANGELOG.md | 3 +++ lib/src/fetch/body.dart | 15 ++++++++++++++- test/body_test.dart | 26 ++++++++++++++++++++++++++ test/public_api_surface_test.dart | 2 ++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cde68c..663c224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Next +- Added `Body.size` for exposing known body byte lengths without consuming the + body. + ## 0.5.0 - BREAKING: `Request.method` and `RequestInit.method` now use `String` values diff --git a/lib/src/fetch/body.dart b/lib/src/fetch/body.dart index e6f6d8e..626582e 100644 --- a/lib/src/fetch/body.dart +++ b/lib/src/fetch/body.dart @@ -41,8 +41,11 @@ class Body extends Stream { Body._({ block.Block? blockHost, Stream? streamHost, + int? byteLength, this.contentType, }) : assert(blockHost != null || streamHost != null), + assert(byteLength == null || byteLength >= 0), + size = byteLength ?? blockHost?.size, _blockHost = blockHost, _streamHost = streamHost; @@ -79,6 +82,7 @@ class Body extends Stream { final encoded = formData.encodeMultipart(); return Body._( streamHost: encoded.stream, + byteLength: encoded.contentLength, contentType: encoded.contentType, ); case final Stream> stream: @@ -103,6 +107,11 @@ class Body extends Stream { /// The body-derived media type, when extracting the body produced one. final String? contentType; + /// The byte length when it is known without consuming the body. + /// + /// Stream-backed bodies created from arbitrary [Stream] values return `null`. + final int? size; + Stream get stream async* { final blockHost = _blockHost; final streamHost = _streamHost; @@ -169,7 +178,11 @@ class Body extends Stream { if (streamHost != null) { final (left, right) = streamTee(streamHost); _streamHost = left; - return Body._(streamHost: right, contentType: contentType); + return Body._( + streamHost: right, + byteLength: size, + contentType: contentType, + ); } throw StateError('Body has no host.'); diff --git a/test/body_test.dart b/test/body_test.dart index 5f107c5..7a274f8 100644 --- a/test/body_test.dart +++ b/test/body_test.dart @@ -55,6 +55,32 @@ void main() { expect(Body([1, 2, 3]).contentType, isNull); }); + test('exposes known byte size without consuming the body', () { + final params = URLSearchParams({'a': '1', 'b': '2'}); + final formData = FormData()..append('a', Multipart.text('1')); + final formBody = Body(formData); + + expect(Body().size, 0); + expect(Body('hello').size, 5); + expect(Body(Uint8List.fromList([1, 2, 3])).size, 3); + expect(Body(Uint8List(2).buffer).size, 2); + expect(Body([1, 2, 3, 4]).size, 4); + expect( + Body(block.Block(['payload'], type: 'text/plain')).size, + 7, + ); + expect(Body(params).size, 7); + expect(formBody.size, greaterThan(0)); + expect(formBody.bodyUsed, isFalse); + }); + + test('reports null size for arbitrary stream bodies', () { + final body = Body(Stream>.value(utf8.encode('stream'))); + + expect(body.size, isNull); + expect(body.bodyUsed, isFalse); + }); + test('block bodies can be converted back to Blob', () async { final body = Body(block.Block(['payload'], type: 'text/plain')); final blob = await body.blob(); diff --git a/test/public_api_surface_test.dart b/test/public_api_surface_test.dart index e547a0e..82c305b 100644 --- a/test/public_api_surface_test.dart +++ b/test/public_api_surface_test.dart @@ -21,6 +21,7 @@ void main() { final file = File([blob], 'hello.txt', type: 'text/plain'); final form = FormData()..append('file', Multipart.blob(file)); final multipart = form.encodeMultipart(boundary: 'api'); + final body = Body('public'); final blockBody = block.Block(['block-body'], type: 'text/plain'); final request = Request( @@ -41,6 +42,7 @@ void main() { expect(requestInit.method, 'POST'); expect(requestInit.priority, RequestPriority.high); expect(responseInit.status, 200); + expect(body.size, 6); expect(request.headers.has('content-type'), isTrue); expect(await multipart.bytes(), isNotEmpty); expect(await response.text(), 'block-body');