How to set a field with multi-value into a Post request?

This command works fine on Linux terminal:

curl -X POST "https://my-api.plantnet.org/v2/identify/all?api-key=11111111111111111111" -H "accept: application/json" -F "organs=flower" -F "organs=leaf" -F "images=@images/image_1.jpeg" -F "images=@images/image_2.jpeg"

As you may have seen there are two multi-value fields,organs and images, one is for String objects and the another is for File objects.

I've made this code:

  static Future<T> postFilesAndGetJson<T>(String url, {List<MapEntry<String, String>> paths, List<MapEntry<String, String>> fields}) async {
    var request = http.MultipartRequest('POST', Uri.parse(url));

    if (paths != null && paths.isNotEmpty) {
      paths.forEach((path) { 
        var file = File.fromUri(Uri.parse(path.value));
        var multipartFile = http.MultipartFile.fromBytes(
          path.key, file.readAsBytesSync(), filename: p.basename(file.path)
        );
        request.files.add(multipartFile);
      });
    }

    if (fields != null && fields.isNotEmpty) {
      request.fields.addEntries(fields);
    }

    return http.Response
      .fromStream(await request.send())
      .then((response) {
        if (response.statusCode == 200) {
          return jsonDecode(response.body) as T;
        }
        print('Status Code : ${response.statusCode}...');
        return null;
      });
  }

And it works fine while field names are different, so for this case it doesn't work because I get status code 400 (Bad Request).

request.fields property is Map<String, String> so I cannot (apparently) set a List<String> as value. Similar case is for request.files.

How to work with multi-value fields?

Upvotes: 2

Views: 668

Answers (1)

Alexey Inkin
Alexey Inkin

Reputation: 2049

  1. The files are actually OK having duplicate field names. The 400 error you get is probably because you send two images but only one organs. So looks like the only thing you need to fix is sending multiple fields of the same name.

  2. Having no better ideas, you may copy the original MultipartRequest and create your own class like MultipartListRequest. Then change fields from a map to a list (changed lines are commented):

import 'dart:convert';
import 'dart:math';

import 'package:http/http.dart';                        // CHANGED
import 'package:http/src/utils.dart';                   // CHANGED
import 'package:http/src/boundary_characters.dart';     // CHANGED

final _newlineRegExp = RegExp(r'\r\n|\r|\n');

class MultipartListRequest extends BaseRequest {        // CHANGED
  /// The total length of the multipart boundaries used when building the
  /// request body.
  ///
  /// According to http://tools.ietf.org/html/rfc1341.html, this can't be longer
  /// than 70.
  static const int _boundaryLength = 70;

  static final Random _random = Random();

  /// The form fields to send for this request.
  final fields = <MapEntry<String, String>>[];          // CHANGED

  /// The list of files to upload for this request.
  final files = <MultipartFile>[];

  MultipartListRequest(String method, Uri url) : super(method, url);

  /// The total length of the request body, in bytes.
  ///
  /// This is calculated from [fields] and [files] and cannot be set manually.
  @override
  int get contentLength {
    var length = 0;

    fields.forEach((field) {                            // CHANGED
      final name = field.key;                           // CHANGED
      final value = field.value;                        // CHANGED

      length += '--'.length +
          _boundaryLength +
          '\r\n'.length +
          utf8.encode(_headerForField(name, value)).length +
          utf8.encode(value).length +
          '\r\n'.length;
    });

    for (var file in files) {
      length += '--'.length +
          _boundaryLength +
          '\r\n'.length +
          utf8.encode(_headerForFile(file)).length +
          file.length +
          '\r\n'.length;
    }

    return length + '--'.length + _boundaryLength + '--\r\n'.length;
  }

  @override
  set contentLength(int? value) {
    throw UnsupportedError('Cannot set the contentLength property of '
        'multipart requests.');
  }

  /// Freezes all mutable fields and returns a single-subscription [ByteStream]
  /// that will emit the request body.
  @override
  ByteStream finalize() {
    // TODO: freeze fields and files
    final boundary = _boundaryString();
    headers['content-type'] = 'multipart/form-data; boundary=$boundary';
    super.finalize();
    return ByteStream(_finalize(boundary));
  }

  Stream<List<int>> _finalize(String boundary) async* {
    const line = [13, 10]; // \r\n
    final separator = utf8.encode('--$boundary\r\n');
    final close = utf8.encode('--$boundary--\r\n');

    for (var field in fields) {                         // CHANGED
      yield separator;
      yield utf8.encode(_headerForField(field.key, field.value));
      yield utf8.encode(field.value);
      yield line;
    }

    for (final file in files) {
      yield separator;
      yield utf8.encode(_headerForFile(file));
      yield* file.finalize();
      yield line;
    }
    yield close;
  }

  /// Returns the header string for a field.
  ///
  /// The return value is guaranteed to contain only ASCII characters.
  String _headerForField(String name, String value) {
    var header =
        'content-disposition: form-data; name="${_browserEncode(name)}"';
    if (!isPlainAscii(value)) {
      header = '$header\r\n'
          'content-type: text/plain; charset=utf-8\r\n'
          'content-transfer-encoding: binary';
    }
    return '$header\r\n\r\n';
  }

  /// Returns the header string for a file.
  ///
  /// The return value is guaranteed to contain only ASCII characters.
  String _headerForFile(MultipartFile file) {
    var header = 'content-type: ${file.contentType}\r\n'
        'content-disposition: form-data; name="${_browserEncode(file.field)}"';

    if (file.filename != null) {
      header = '$header; filename="${_browserEncode(file.filename!)}"';
    }
    return '$header\r\n\r\n';
  }

  /// Encode [value] in the same way browsers do.
  String _browserEncode(String value) =>
      // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
      // field names and file names, but in practice user agents seem not to
      // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as
      // `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII
      // characters). We follow their behavior.
      value.replaceAll(_newlineRegExp, '%0D%0A').replaceAll('"', '%22');

  /// Returns a randomly-generated multipart boundary string
  String _boundaryString() {
    var prefix = 'dart-http-boundary-';
    var list = List<int>.generate(
        _boundaryLength - prefix.length,
        (index) =>
            boundaryCharacters[_random.nextInt(boundaryCharacters.length)],
        growable: false);
    return '$prefix${String.fromCharCodes(list)}';
  }
}

(Would be better to subclass, but many valuable things are private there.)

  1. Then in your code set the fields using addAll instead of addEntries:
request.fields.addAll(fields);

I see that you have already submitted an issue to Dart http package. This is good.

Upvotes: 2

Related Questions