Skip to content

HTTP

Downloading files with progress

The best way to download a file is to use File.openWrite and a streamed request. This allows the file to be read in chunks instead of all at once, saving you from dangrous out of memory errors:

import 'dart:io';  

import 'package:http/http.dart';

Future<void> download(
  File file,
  StreamedResponse response, {
  void Function(int, int?)? onProgress,
}) async {
  final sink = file.openWrite();
  if (onProgress == null) {
    await sink.addStream(response.stream);
  } else {
    var bytes = 0;
    await sink.addStream(response.stream.map((e) {
      bytes += e.length;
      onProgress(bytes, response.contentLength);
      return e;
    }));
  }
  await sink.flush();
  await sink.close();
}

void main() async {
  final request = Request('GET', Uri.parse('https://i.imgur.com/img0gF3.jpg'));
  final response = await request.send();
  final file = File('birb_slenp.jpg');
  await download(file, response, onProgress: (bytes, total) {
    // Some servers don't send a Content-Length
    if (total != null) {
      print('${(100 * (bytes / total)).round()}%');
    }
  });
}

Uploading files with progress

Uploading is a similar story, we use File.openRead to read the file and a StreamedRequest to pipe it to the server in chunks:

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart';

Future<StreamedResponse> upload(
  File file,
  StreamedRequest request, {
  void Function(int, int)? onProgress,
}) async {
  final stream = file.openRead();
  final total = file.lengthSync();
  final sink = request.sink as StreamSink<List<int>>;
  request.contentLength = total;
  if (onProgress == null) {
    unawaited(stream.pipe(sink));
  } else {
    var bytes = 0;
    unawaited(stream.map((e) {
      bytes += e.length;
      onProgress(bytes, total);
      return e;
    }).pipe(sink));
  }
  return await request.send();
}

void main() async {
  final request = StreamedRequest(
    'POST',
    Uri.parse('http://localhost:621/upload'),
  );
  final file = File('birb_slenp.jpg');
  request.headers['content-type'] = 'image/jpeg';
  final response = await upload(file, request, onProgress: (bytes, total) {
    print('${(100 * (bytes / total)).round()}%');
  });
  print('statusCode: ${response.statusCode}');
  print('body: ${utf8.decode(await response.stream.toBytes())}');
}

Multipart request progress

Again same as before, we can use openRead and map to count the bytes:

MultipartFile multipartFileWithProgress(  
  String field,  
  File file,  
  void Function(int, int) onProgress,  
) {  
  final total = file.lengthSync();  
  var bytes = 0;  
  return MultipartFile(  
    field,  
    file.openRead().map((e) {  
      bytes += e.length;  
      onProgress(bytes, total);  
      return e;  
    }),  
    total,  
  );  
}

Error handling

For error handling I prefer using a custom exception class to cut down boilerplate and make it easier to surface useful errors to the frontend.

After every request you would call HttpException.ensureSuccess(response) which throws a formatted exception when the response does not have a 2xx status code.


Created May 7, 2023
Updated May 9, 2023