-
Notifications
You must be signed in to change notification settings - Fork 22
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
Add implementation of FormData
builtin
#191
Conversation
7b1e6a1
to
0a65a1e
Compare
This one is pretty much ready for a review. I will keep it as a draft until dependency PRs are merged. |
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 is looking great! There are a bunch of things still to address, but I don't think it'll take too much more work
tests/wpt-harness/expectations/fetch/api/request/request-init-002.any.js.json
Show resolved
Hide resolved
tests/wpt-harness/expectations/fetch/api/response/response-init-002.any.js.json
Show resolved
Hide resolved
tests/wpt-harness/expectations/xhr/formdata/set-blob.any.js.json
Outdated
Show resolved
Hide resolved
Hey! I have a question regarding encoding
Does this mean that we need to implement a stream encoder for const blobParts = [];
for (const { 0: name, 1: value } of formData) {
blobParts.push(encodeBoundary(name));
blobParts.push(value);
}
const blob = new Blob(blobParts, { type: "multipart/form-data"});
...
stream = blob.stream();
length = blob.size
contentType = blob.type; I think the problem with that approach is that constructing a new blob is blocking, e.g. if value is a |
Afaict, avoiding the extra copy would involve implementing a custom FormDataStream or similar exactly as you say, similarly to what you did with the blob streaming. Perhaps there are parts of the low-level code that can be abstracted / shared? I guess it depends on the performance requirements - if users have a lot of data, copying everything would not only have the compute cost of the copy but also the memory cost of duplicating these items. Nice to avoid if we can? |
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.
Getting even closer! I once again left a bunch of comments, because this is surprisingly subtle. And would like to take another look once those are addressed—but that's not on you, it's on the spec being too subtle.
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-form-data-set | ||
// | ||
// Note: all uses of `create-an-entry` immediately append it, too, so that part is folded in here. | ||
bool FormData::append(JSContext *cx, HandleObject self, std::string_view name, HandleValue 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.
(Leaving a comment on the overall function here, because I'm not sure were exactly inside the function body to leave it.)
The spec for this algorithm is a little bit weird, unfortunately, making it hard to understand what exactly is going on.
In particular, the sub-steps of step 3, interpreted precisely, would in some cases require first, in step 3.1, creating a new File
instance with the name "blob"
, and then immediately, in step 3.2, creating another new File
instance with filename
as the name. Since creating a File
instance doesn't have observable side-effects, that is technically okay, but of course wasteful. It also confuses what actually should happen, which is one of three things:
- if
value
is astring
, ensure that bothname
andvalue
are scalar value strings, and append an entry of{ name, value }
, and ignorefilename
- otherwise, if
value
is aBlob
but not aFile
, or iffilename
is given, create a newFile
instance and append an entry of{ name, new_file }
- otherwise, append an entry of
{ name, value }
I think it might make sense to restructure this function to follow that logic. In that case, I would still keep the spec step comments, but add a comment explaining the restructuring.
I know I'm giving you a lot of feedback on this one function; the reason is that the spec really is very subtle here, and I want to make sure that we both get it right, and put ourselves into a position where we can understand what's going on long after we've all forgotten this discussion :)
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.
Right, but we still need to handle the filename
in the second step and the logic for this should be something like this:
if value
is a Blob
but not a File
, or if filename
is given, then:
let new_file = match (is_file, maybe_filename) {
(false, Some(filename)) => File(filename),
(true, Some(filename)) => File(filename),
(false, None) => File("blob"),
(true, None) => unreachable!(),
};
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.
E.g: these are all the cases for bullets 2 and 3:
const value = new Blob();
formData.set("blob-3", value, "custom name");
const blob3 = formData.get("blob-3");
assert_equals(blob3.name, "custom name");
const value = new File([], "name");
formData.set("file-2", value, "custom name");
const file2 = formData.get("file-2");
assert_equals(file2.name, "custom name");
const value = new Blob();
formData.set("blob-1", value);
const blob1 = formData.get("blob-1");
assert_equals(blob1.name, "blob");
const value = new File([], "name");
formData.set("file-1", value);
const file1 = formData.get("file-1");
assert_equals(file1.name, "name");
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 papered over the filename
handling, yes. What I meant to highlight is that the key choice here is whether to create a new File
or not, and that whether to use "blob"
or filename
is a nested choice in that. I.e., I'm arguing for a different factoring from you match
expression, because whether or not to create a new File
makes substantially more difference than what name to use.
Overall, the cleanest structure is probably
if (!Blob::is_instance(value)) {
// treat `value` as a string and return early
}
if (File::is_instance(value) && filename.isUndefined()) {
// append `value` as is and return early
}
RootedValue filename_val(cx);
if (!filename.isUndefined()) {
filename_val = // scalar version of `filename`
} else {
filename_val = // "blob"
}
// Create the new file using `filename_val`
Does that make sense?
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.
Very nice! Thank you for your patience with the long review cycles, and for getting this into great shape!
This patch adds implementation of
FormaData
: https://xhr.spec.whatwg.org/#interface-formdataI will add support for Request/Response
FormData
in a separate PR.Depends on #181