-
Notifications
You must be signed in to change notification settings - Fork 381
Implement multipart forms #113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@nex3 currently work in progress for the implementation. I have tests running locally but won't push that until we figure out if this is what you want. Another option rather than implementing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The overall architecture here looks good!
lib/src/boundary_characters.dart
Outdated
|
||
/// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Documentation comments should start with a single-sentence paragraph.
lib/src/boundary_characters.dart
Outdated
/// 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. | ||
const int BOUNDARY_LENGTH = 70; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Constants should be lower camel case. Same for BOUNDARY_CHARACTERS
.
I'd also omit the int
here.
lib/src/boundary_characters.dart
Outdated
@@ -2,6 +2,8 @@ | |||
// for details. All rights reserved. Use of this source code is governed by a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why this was ever a separate library... I'd just put this stuff in multipart_request.dart
.
lib/src/boundary_characters.dart
Outdated
|
||
final Random _random = new Random(); | ||
|
||
/// Returns a randomly-generated multipart boundary string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Period at the end of the sentence.
lib/src/multipart_file.dart
Outdated
@@ -44,20 +40,20 @@ class MultipartFile { | |||
/// future may be inferred from [filename]. | |||
MultipartFile(this.field, Stream<List<int>> stream, this.length, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about consolidating these constructors? I'm thinking:
new MultipartFile(String field, body, ...)
which takes either aList<int>
or aString
,new MultipartFile.fromStream(String field, stream, int length)
which takes either aStream<List<int>>
or aStream<String>
,static Future<MultipartFile> loadStream(String field, stream)
which takes either aStream<List<int>>
or aStream<String>
and drains it to determine the length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've always kinda preferred the explicit constructors rather than using dynamic
since there are no union types. I do like the loadStream
idea rather than fromPath
.
Would fromStream
be possible with a Stream<String>
though as there would need to be some kind of encoding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've always kinda preferred the explicit constructors rather than using
dynamic
since there are no union types. I do like theloadStream
idea rather thanfromPath
.
I disagree. That imposes more writing on the user for minimal static analysis gains. Also, union types will almost certainly happen eventually so it's a good idea to plan APIs around them now where possible.
Would
fromStream
be possible with aStream<String>
though as there would need to be some kind of encoding?
I suppose we'd need an optional Encoding
parameter like we do for the Request
constructors.
lib/src/multipart_request.dart
Outdated
} | ||
void writeLine() { | ||
controller.add([13, 10]); // \r\n | ||
contentLength += 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're only using these for fields now, you might as well just add everything to a single Uint8List
rather than adding separate chunks to a controller. The typed_data
package can help with that.
lib/src/multipart_request.dart
Outdated
header.addAll(UTF8.encode(_headerForFile(file))); | ||
contentLength += header.length + file.length + 2; | ||
fileHeaders[i] = header; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment explaining that we compute the headers ahead of time so we can synchronously access the length.
lib/src/multipart_request.dart
Outdated
// used to get the fileHeaders | ||
var i = 0; | ||
|
||
Future.forEach(files, (file) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider pulling this into a static method so you can make it async and just use a normal for
loop.
lib/src/multipart_request.dart
Outdated
// characters). We follow their behavior. | ||
return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22"); | ||
/// Returns the header string for a field. | ||
String _headerForField(String name, String value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally make private methods that are only used by one class members of that class, even if they don't access its state directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are accessed from a factory constructor so I put them below. Do you want them as static class methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
lib/src/request.dart
Outdated
var boundary = boundaryString(); | ||
|
||
headers ??= <String, String>{}; | ||
headers['content-type'] = 'multipart/form-data; boundary=$boundary'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't destructively modify the user's headers
map.
52b79bc
to
6029e43
Compare
@nex3 https://pub.dartlang.org/packages/mime yay or nay? |
2097129
to
6029e43
Compare
6029e43
to
20afe26
Compare
lib/src/multipart_file.dart
Outdated
@@ -44,20 +40,20 @@ class MultipartFile { | |||
/// future may be inferred from [filename]. | |||
MultipartFile(this.field, Stream<List<int>> stream, this.length, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've always kinda preferred the explicit constructors rather than using
dynamic
since there are no union types. I do like theloadStream
idea rather thanfromPath
.
I disagree. That imposes more writing on the user for minimal static analysis gains. Also, union types will almost certainly happen eventually so it's a good idea to plan APIs around them now where possible.
Would
fromStream
be possible with aStream<String>
though as there would need to be some kind of encoding?
I suppose we'd need an optional Encoding
parameter like we do for the Request
constructors.
lib/src/multipart_body.dart
Outdated
import 'multipart_file.dart'; | ||
import 'utils.dart'; | ||
|
||
class MultipartBody implements Body { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document this
lib/src/multipart_body.dart
Outdated
@@ -0,0 +1,147 @@ | |||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: 2017
lib/src/multipart_body.dart
Outdated
|
||
/// The length of the stream returned by [read]. | ||
/// | ||
/// This is calculated from the fields and files passed into the body. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You generally don't have to document inherited members, since they're documented in the parent class.
lib/src/multipart_body.dart
Outdated
|
||
/// Creates a [MultipartBody] from the given [fields] and [files]. | ||
/// | ||
/// The [boundary] is used to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfinished docs
test/utils.dart
Outdated
@@ -66,6 +67,40 @@ class _Parse extends Matcher { | |||
} | |||
} | |||
|
|||
/// A matcher that validates the body of a multipart request after finalization. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: First sentence should be its own paragraph.
test/utils.dart
Outdated
/// The string "{{boundary}}" in [pattern] will be replaced by the boundary | ||
/// string for the request, and LF newlines will be replaced with CRLF. | ||
/// Indentation will be normalized. | ||
Matcher bodyMatches(String pattern) => new _BodyMatches(pattern); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name is pretty generic for something that's specifically designed to work with multipart requests. Maybe multipartBodyMatches()
?
test/utils.dart
Outdated
bool matches(item, Map matchState) { | ||
if (item is! http.Request) return false; | ||
|
||
var future = collectBytes(item.read()).then((bodyBytes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use Request.readAsString()
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of this check. The string length isn't necessarily the same as the contentLength at this point.
expect(item.contentLength, equals(bodyBytes.length));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... it might be nice to add Request.readAsBytes()
.
test/multipart_test.dart
Outdated
@@ -0,0 +1,249 @@ | |||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2017
--{{boundary}}-- | ||
''')); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these ported directly from the old API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep they're from the 0.11.x branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully this commit resolves everything but the constructors.
mime
was just pushed so I'll integrate that when going through that.
test/utils.dart
Outdated
bool matches(item, Map matchState) { | ||
if (item is! http.Request) return false; | ||
|
||
var future = collectBytes(item.read()).then((bodyBytes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of this check. The string length isn't necessarily the same as the contentLength at this point.
expect(item.contentLength, equals(bodyBytes.length));
--{{boundary}}-- | ||
''')); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep they're from the 0.11.x branch.
pubspec.yaml
Outdated
@@ -8,6 +8,7 @@ dependencies: | |||
collection: "^1.5.0" | |||
http_parser: ">=0.0.1 <4.0.0" | |||
path: ">=0.9.0 <2.0.0" | |||
typed_data: "^1.1.5" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for my own reference are we going to want to tighten the restriction for strong mode compliance?
lib/src/request.dart
Outdated
Iterable<MultipartFile> files}) { | ||
fields ??= <String, String>{}; | ||
files ??= <MultipartFile>[]; | ||
headers ??= <String, String>{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will end up getting set when a HttpUnmodifiableMap
is created within the Message
constructor so no it doesn't need one.
test/utils.dart
Outdated
bool matches(item, Map matchState) { | ||
if (item is! http.Request) return false; | ||
|
||
var future = collectBytes(item.read()).then((bodyBytes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... it might be nice to add Request.readAsBytes()
.
pubspec.yaml
Outdated
@@ -8,6 +8,7 @@ dependencies: | |||
collection: "^1.5.0" | |||
http_parser: ">=0.0.1 <4.0.0" | |||
path: ">=0.9.0 <2.0.0" | |||
typed_data: "^1.1.5" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally don't do that. This package doesn't depend on the fact that its dependencies are strong-mode compliant; that's a property of the root package, if anything.
lib/src/multipart_body.dart
Outdated
controller.add(fileHeaders[i]); | ||
try { | ||
await writeStreamToSink(files[i].read(), controller); | ||
} on Exception catch (exception, stackTrace) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave off the on
clause here. We want to catch anything that gets thrown.
lib/src/multipart_body.dart
Outdated
try { | ||
await writeStreamToSink(files[i].read(), controller); | ||
} on Exception catch (exception, stackTrace) { | ||
controller.addError(exception, stackTrace); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document that we're catching here because file.read()
can throw synchronously.
lib/src/multipart_file.dart
Outdated
bool get isFinalized => _isFinalized; | ||
bool _isFinalized = false; | ||
if (value is String) { | ||
encoding ??= encodingForMediaType(contentType) ?? UTF8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like with the request body, I'd like the value encoding to come only from the encoding
parameter, not from the contentType. This ensures that filling in the defaults flows only one way: contentType
always looks at encoding
for its default value, not the other way around.
Also, we should only set the encoding to UTF8—and thus add a charset parameter to the content type—if the value isn't plain ASCII.
lib/src/multipart_file.dart
Outdated
|
||
/// Looks up the [MediaType] from the [filename]'s extension or from | ||
/// magic numbers contained within a file header's [bytes]. | ||
static MediaType _lookupMediaType(String filename, [List<int> bytes]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: _lookUpMediaType
"Lookup" is a word, but it's only a noun; the verb is always two words.
lib/src/multipart_file.dart
Outdated
/// Looks up the [MediaType] from the [filename]'s extension or from | ||
/// magic numbers contained within a file header's [bytes]. | ||
static MediaType _lookupMediaType(String filename, [List<int> bytes]) { | ||
if ((filename == null) && (bytes == null)) return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: unnecessary parens.
lib/src/multipart_file.dart
Outdated
if ((filename == null) && (bytes == null)) return null; | ||
|
||
// lookupMimeType expects filename to be non-null but its possible that | ||
// this can be called with bytes but no filename. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you file an issue against the mime package for this?
lib/src/request.dart
Outdated
Request._(this.method, this.url, | ||
body, | ||
Encoding encoding, | ||
/// Creates a new `multipart/form-data` [Request] to [url], which can be a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider linking multipart/form-data
to an explanation of the format (maybe Wikipedia).
pubspec.yaml
Outdated
@@ -7,7 +7,8 @@ dependencies: | |||
async: "^1.13.0" | |||
collection: "^1.5.0" | |||
http_parser: ">=0.0.1 <4.0.0" | |||
path: ">=0.9.0 <2.0.0" | |||
mime: ">=0.9.0 < 1.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^0.9.0
With this version constraint, mime
could release a totally breaking 0.10.0
release and we'd still declare compatibility with it.
Looks good! Thanks for all your hard work on this 😄. |
Implement multipart form handling.
Fixes #97