Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ See the [Migration Guide][] for the complete breaking changes list.**

## Unreleased

- Fix `FormData.readAsBytes` excessive memory usage with large payloads by replacing the O(n²) `reduce`+spread approach with a pre-allocated `Uint8List`.
- Fix request hanging indefinitely when async interceptor callbacks throw without calling the handler.
- Fix `HttpException: Connection closed before full header was received` being reported as `DioExceptionType.unknown`.
- Add `transformTimeout` to bound long-running response transformations, including background JSON decoding.
Expand Down
12 changes: 8 additions & 4 deletions dio/lib/src/form_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,14 @@ class FormData {
}

/// Transform the entire FormData contents as a list of bytes asynchronously.
Future<Uint8List> readAsBytes() {
return Future.sync(
() => finalize().reduce((a, b) => Uint8List.fromList([...a, ...b])),
);
Future<Uint8List> readAsBytes() async {
final result = Uint8List(length);
int offset = 0;
await for (final chunk in finalize()) {
result.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
return result;
}

/// Clones this finalized [FormData] so it can be re-sent, for example when
Expand Down
69 changes: 69 additions & 0 deletions dio/test/formdata_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:dio/dio.dart';
import 'package:test/test.dart';

import 'mock/adapters.dart';

int _indexOfSubList(List<int> bytes, List<int> pattern, [int start = 0]) {
if (pattern.isEmpty) {
return start <= bytes.length ? start : -1;
}

for (int i = start; i <= bytes.length - pattern.length; i++) {
bool matched = true;
for (int j = 0; j < pattern.length; j++) {
if (bytes[i + j] != pattern[j]) {
matched = false;
break;
}
}
if (matched) {
return i;
}
}

return -1;
}

void main() async {
group(FormData, () {
test(
Expand Down Expand Up @@ -267,6 +289,53 @@ void main() async {
expect(result, contains('name="api[data][c]"'));
});

test('readAsBytes keeps chunked file payload complete', () async {
const chunks = 128;
const chunkSize = 1024;
final expectedFileLength = chunks * chunkSize;
final expectedPayload = Uint8List.fromList(
List<int>.generate(
expectedFileLength,
(i) => (i ~/ chunkSize) % 256,
),
);

final file = MultipartFile.fromStream(
() => Stream<List<int>>.fromIterable(
List.generate(
chunks,
(i) => List<int>.filled(chunkSize, i % 256),
),
),
expectedFileLength,
filename: 'chunked.bin',
);

final fd = FormData.fromMap({'file': file});
final data = await fd.readAsBytes();
final headerStart = _indexOfSubList(
data,
utf8.encode('filename="chunked.bin"'),
);
final headerEnd = _indexOfSubList(
data,
utf8.encode('\r\n\r\n'),
headerStart,
);
final payloadStart = headerEnd + 4;
final payloadEnd = payloadStart + expectedPayload.length;
final trailingBoundary = utf8.encode('\r\n--${fd.boundary}--\r\n');

expect(data.length, fd.length);
expect(headerStart, isNonNegative);
expect(headerEnd, isNonNegative);
expect(
data.sublist(payloadStart, payloadEnd),
orderedEquals(expectedPayload),
);
expect(data.sublist(payloadEnd), orderedEquals(trailingBoundary));
});

test('posts maps correctly', () async {
final fd = FormData.fromMap(
{
Expand Down
Loading