dart-lang / http

A composable API for making HTTP requests in Dart.
https://pub.dev/packages/http
BSD 3-Clause "New" or "Revised" License
1.03k stars 358 forks source link

Header for file not correct in multipart request and headers in reverse order? #368

Open gokkep opened 4 years ago

gokkep commented 4 years ago

Reading RFC 6266 and RFC 2388 and looking at my Eiffel web framework server side library code, to handle uploading of files using multipart/form-data it seems something is off somewhere.

It seems that the class MultipartRequest from your library is doing it wrong and I want to find out if this is true.

When a browser sends a file through multipart/form-data, the header and second line of the content type is send as follows:

Content-Disposition: form-data; name="uploaded_file[]"; filename="Apps.png" Content-Type: image/png

But when I upload a file using the http dart library, I receive:

content-type: image/png content-disposition: form-data; name="uploaded_file[]"; filename="Apps.png"

Two things are noticable:

  1. The case of the header, e.g. content-type instead of Content-Type
  2. The two are in reverse order, content-type first should be content-disposition first.

I think this is wrong and should both be modified.

Can someone check if my statements are true and when due, please change them as soon as possible.

Regards, Paul

Xt-Man commented 4 years ago

Hi @gokkep , i'm facing the same issue. Do you discovered a workaround?

Thx

gokkep commented 4 years ago

Hi Xt-Man, well no not really. I changed the Eiffel code to support both cases, but it is a hack. This should be solved in the proper way. Shamefull no one of this development team seems to react upon this issue after such long time. Anyone???

Xt-Man commented 4 years ago

Hi @gokkep , i created my own MultipartRequest, works for me, you can try it, let me now if works for you too. MyMultipartRequest.dart `import 'dart:convert'; import 'dart:math';

import 'package:http/http.dart' as http;

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

const List BOUNDARY_CHARACTERS = [ 43, 95, 45, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122 ];

class MyMultipartRequest extends http.MultipartRequest {

static const int _boundaryLength = 70;

static final Random _random = Random();

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

@override int get contentLength { var length = 0;

fields.forEach((name, value) {
  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;

}

String _headerForFile(http.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)}"';
// }

var header = 'Content-Disposition: form-data; name="${_browserEncode(file.field)}"';
if (file.filename != null) {
  header = '$header; filename="${_browserEncode(file.filename)}"';
}
header = '$header\r\n'
  'Content-Type: ${file.contentType}';

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. return value.replaceAll(_newlineRegExp, '%0D%0A').replaceAll('"', '%22'); }

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'; }

final _asciiOnly = RegExp(r'^[\x00-\x7F]+$');

bool isPlainAscii(String string) => _asciiOnly.hasMatch(string);

@override http.ByteStream finalize() { // TODO: freeze fields and files final boundary = _boundaryString(); headers['content-type'] = 'multipart/form-data; boundary=$boundary'; super.finalize(); return http.ByteStream(_finalize(boundary)); }

Stream<List> _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.entries) {
  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;

}

String _boundaryString() { var prefix = 'dart-http-boundary-'; var list = List.generate( _boundaryLength - prefix.length, (index) => BOUNDARY_CHARACTERS[_random.nextInt(BOUNDARY_CHARACTERS.length)], growable: false); return '$prefix${String.fromCharCodes(list)}'; }

}`

rajan995 commented 3 years ago

please use this code its working fine. fileUpload(Request request, String id) async { List dataBytes = [];

await for (var data in request.read()) {
  dataBytes.addAll(data);
}
print(request.headers['content-type']);
var header = HeaderValue.parse(request.headers['content-type'].toString());
var boundary = header.parameters['boundary'] as String;
final transform = MimeMultipartTransformer(boundary);
final bodyStream = Stream.fromIterable([dataBytes]);

final parts = await transform.bind(bodyStream).toList();

print(parts);
for (var part in parts) {
  print(part.headers);
  final contentDisposition = part.headers['content-disposition'];

  final content = await part.toList();
  await File('$id.jpg').writeAsBytes(content[0]);
}

return Response.ok("ok");

}

natebosch commented 3 years ago
  1. The case of the header, e.g. content-type instead of Content-Type

I filed https://github.com/dart-lang/http/issues/609 as the issue to track support for preserving header case.

2. The two are in reverse order, content-type first should be content-disposition first.

Can you point to where this is specified?

xia-weiyang commented 2 years ago

i'm facing the same issue.