LibWeb: Implement formData() for "multipart/form-data"

This commit is contained in:
Feng Yu 2024-12-24 19:11:33 -08:00
parent 6fd7b9b6b9
commit ef339252cf
26 changed files with 1355 additions and 4 deletions

View file

@ -5,6 +5,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/GenericLexer.h>
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/Completion.h>
@ -16,10 +17,13 @@
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOMURL/URLSearchParams.h>
#include <LibWeb/Fetch/Body.h>
#include <LibWeb/Fetch/Infrastructure/HTTP.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/FileAPI/File.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/Infra/JSON.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/WebIDL/Promise.h>
@ -137,10 +141,15 @@ WebIDL::ExceptionOr<JS::Value> package_data(JS::Realm& realm, ByteBuffer bytes,
case PackageDataType::FormData:
// If mimeTypes essence is "multipart/form-data", then:
if (mime_type.has_value() && mime_type->essence() == "multipart/form-data"sv) {
// FIXME: 1. Parse bytes, using the value of the `boundary` parameter from mimeType, per the rules set forth in Returning Values from Forms: multipart/form-data. [RFC7578]
// FIXME: 2. If that fails for some reason, then throw a TypeError.
// FIXME: 3. Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list.
return JS::js_null();
// 1. Parse bytes, using the value of the `boundary` parameter from mimeType, per the rules set forth in Returning Values from Forms: multipart/form-data. [RFC7578]
auto error_or_entry_list = parse_multipart_form_data(realm, bytes, mime_type.value());
// 2. If that fails for some reason, then throw a TypeError.
if (error_or_entry_list.is_error())
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Failed to parse multipart form data: {}", error_or_entry_list.release_error().message)) };
// 3. Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list.
return TRY(XHR::FormData::create(realm, error_or_entry_list.release_value()));
}
// Otherwise, if mimeTypes essence is "application/x-www-form-urlencoded", then:
else if (mime_type.has_value() && mime_type->essence() == "application/x-www-form-urlencoded"sv) {
@ -231,4 +240,232 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> consume_body(JS::Realm& realm, Bod
return promise;
}
// https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name
static MultipartParsingErrorOr<String> parse_multipart_form_data_name(GenericLexer& lexer)
{
// 1. Assert: The byte at (position - 1) is 0x22 (").
VERIFY(lexer.peek(-1) == '"');
// 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position.
auto name = lexer.consume_until(is_any_of("\n\r\""sv));
// 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
if (!lexer.consume_specific('"'))
return MultipartParsingError { MUST(String::formatted("Expected \" at position {}", lexer.tell())) };
// 4. Replace any occurrence of the following subsequences in name with the given byte:
// - "%0A" with 0x0A (LF)
// - "%0D" with 0x0D (CR)
// - "%22" with 0x22 (")
StringBuilder builder;
for (size_t i = 0; i < name.length(); ++i) {
// Check for subsequences starting with '%'
if (name[i] == '%' && i + 2 < name.length()) {
auto subsequence = name.substring_view(i, 3);
if (subsequence == "%0A"sv) {
builder.append(0x0A); // Append LF
i += 2; // Skip the next two characters
continue;
}
if (subsequence == "%0D"sv) {
builder.append(0x0D); // Append CR
i += 2; // Skip the next two characters
continue;
}
if (subsequence == "%22"sv) {
builder.append(0x22); // Append "
i += 2; // Skip the next two characters
continue;
}
}
// Append the current character if no substitution was made
builder.append(name[i]);
}
return builder.to_string_without_validation();
}
// https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
static MultipartParsingErrorOr<MultiPartFormDataHeader> parse_multipart_form_data_header(GenericLexer& lexer)
{
// 1. Let name, filename and contentType be null.
MultiPartFormDataHeader header;
// 2. While true:
while (true) {
// 1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
if (lexer.next_is("\r\n"sv)) {
// 1. If name is null, return failure.
if (!header.name.has_value())
return MultipartParsingError { "Missing name parameter in Content-Disposition header"_string };
// 2. Return name, filename and contentType.
return header;
}
// 2. Let header name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
auto header_name = lexer.consume_until(is_any_of("\n\r:"sv));
// 3. Remove any HTTP tab or space bytes from the start or end of header name.
header_name = header_name.trim(Infrastructure::HTTP_TAB_OR_SPACE, TrimMode::Both);
// 4. If header name does not match the field-name token production, return failure.
if (!Infrastructure::is_header_name(header_name.bytes()))
return MultipartParsingError { MUST(String::formatted("Invalid header name {}", header_name)) };
// 5. If the byte at position is not 0x3A (:), return failure.
// 6. Advance position by 1.
if (!lexer.consume_specific(':'))
return MultipartParsingError { MUST(String::formatted("Expected : at position {}", lexer.tell())) };
// 7. Collect a sequence of bytes that are HTTP tab or space bytes given position. (Do nothing with those bytes.)
lexer.ignore_while(Infrastructure::is_http_tab_or_space);
// 8. Byte-lowercase header name and switch on the result:
// -> `content-disposition`
if (header_name.equals_ignoring_ascii_case("content-disposition"sv)) {
// 1. Set name and filename to null.
header.name.clear();
header.filename.clear();
// 2. If position does not point to a sequence of bytes starting with `form-data; name="`, return failure.
// 3. Advance position so it points at the byte after the next 0x22 (") byte (the one in the sequence of bytes matched above).
if (!lexer.consume_specific("form-data; name=\""sv))
return MultipartParsingError { MUST(String::formatted("Expected `form-data; name=\"` at position {}", lexer.tell())) };
// 4. Set name to the result of parsing a multipart/form-data name given input and position, if the result is not failure. Otherwise, return failure.
auto maybe_name = parse_multipart_form_data_name(lexer);
if (maybe_name.is_error())
return maybe_name.release_error();
header.name = maybe_name.release_value();
// 5. If position points to a sequence of bytes starting with `; filename="`:
// 1. Advance position so it points at the byte after the next 0x22 (") byte (the one in the sequence of bytes matched above).
if (lexer.consume_specific("; filename=\""sv)) {
// 2. Set filename to the result of parsing a multipart/form-data name given input and position, if the result is not failure. Otherwise, return failure.
auto maybe_filename = parse_multipart_form_data_name(lexer);
if (maybe_filename.is_error())
return maybe_filename.release_error();
header.filename = maybe_filename.release_value();
}
}
// -> `content-type`
else if (header_name.equals_ignoring_ascii_case("content-type"sv)) {
// 1. Let header value be the result of collecting a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
auto header_value = lexer.consume_until(Infrastructure::is_http_newline);
// 2. Remove any HTTP tab or space bytes from the end of header value.
header_value = header_value.trim(Infrastructure::HTTP_TAB_OR_SPACE, TrimMode::Right);
// 3. Set contentType to the isomorphic decoding of header value.
header.content_type = Infra::isomorphic_decode(header_value.bytes());
}
// -> Otherwise
else {
// 1. Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position. (Do nothing with those bytes.)
lexer.ignore_until(Infrastructure::is_http_newline);
}
// 9. If position does not point to a sequence of bytes starting with 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
if (!lexer.consume_specific("\r\n"sv))
return MultipartParsingError { MUST(String::formatted("Expected CRLF at position {}", lexer.tell())) };
}
return header;
}
// https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
MultipartParsingErrorOr<Vector<XHR::FormDataEntry>> parse_multipart_form_data(JS::Realm& realm, StringView input, MimeSniff::MimeType const& mime_type)
{
// 1. Assert: mimeTypes essence is "multipart/form-data".
VERIFY(mime_type.essence() == "multipart/form-data"sv);
// 2. If mimeTypes parameters["boundary"] does not exist, return failure. Otherwise, let boundary be the result of UTF-8 decoding mimeTypes parameters["boundary"].
auto maybe_boundary = mime_type.parameters().get("boundary"sv);
if (!maybe_boundary.has_value())
return MultipartParsingError { "Missing boundary parameter in Content-Type header"_string };
auto boundary = maybe_boundary.release_value();
// 3. Let entry list be an empty entry list.
Vector<XHR::FormDataEntry> entry_list;
// 4. Let position be a pointer to a byte in input, initially pointing at the first byte.
GenericLexer lexer(input);
auto boundary_with_dashes = MUST(String::formatted("--{}", boundary));
// 5. While true:
while (true) {
// 1. If position points to a sequence of bytes starting with 0x2D 0x2D (`--`) followed by boundary, advance position by 2 + the length of boundary. Otherwise, return failure.
if (!lexer.consume_specific(boundary_with_dashes))
return MultipartParsingError { MUST(String::formatted("Expected `--` followed by boundary at position {}", lexer.tell())) };
// 2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A (`--` followed by CR LF) followed by the end of input, return entry list.
if (lexer.next_is("--\r\n"sv))
return entry_list;
// 3. If position does not point to a sequence of bytes starting with 0x0D 0x0A (CR LF), return failure.
// 4. Advance position by 2. (This skips past the newline.)
if (!lexer.consume_specific("\r\n"sv))
return MultipartParsingError { MUST(String::formatted("Expected CRLF at position {}", lexer.tell())) };
// 5. Let name, filename and contentType be the result of parsing multipart/form-data headers on input and position, if the result is not failure. Otherwise, return failure.
auto header = TRY(parse_multipart_form_data_header(lexer));
// 6. Advance position by 2. (This skips past the empty line that marks the end of the headers.)
lexer.ignore(2);
// 7. Let body be the empty byte sequence.
// 8. Body loop: While position is not past the end of input:
// 1. Append the code point at position to body.
// 2. If body ends with boundary:
// 1. Remove the last 4 + (length of boundary) bytes from body.
// 2. Decrease position by 4 + (length of boundary).
// 3. Break out of body loop.
auto body = lexer.consume_until(boundary_with_dashes.bytes_as_string_view());
if (lexer.next_is(boundary_with_dashes.bytes_as_string_view())) {
constexpr size_t trailing_crlf_length = 2;
if (body.length() >= trailing_crlf_length) {
body = body.substring_view(0, body.length() - trailing_crlf_length);
lexer.retreat(trailing_crlf_length);
}
}
// 9. If position does not point to a sequence of bytes starting with 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
if (!lexer.consume_specific("\r\n"sv))
return MultipartParsingError { MUST(String::formatted("Expected CRLF at position {}", lexer.tell())) };
// 10. If filename is not null:
Optional<XHR::FormDataEntryValue> value;
if (header.filename.has_value()) {
// 1. If contentType is null, set contentType to "text/plain".
if (!header.content_type.has_value())
header.content_type = "text/plain"_string;
// 2. If contentType is not an ASCII string, set contentType to the empty string.
if (!all_of(header.content_type->code_points(), is_ascii)) {
header.content_type = ""_string;
}
// 3. Let value be a new File object with name filename, type contentType, and body body.
auto blob = FileAPI::Blob::create(realm, MUST(ByteBuffer::copy(body.bytes())), header.content_type.release_value());
FileAPI::FilePropertyBag options {};
options.type = blob->type();
auto file = MUST(FileAPI::File::create(realm, { GC::make_root(blob) }, header.filename.release_value(), move(options)));
value = GC::make_root(file);
}
// 11. Otherwise:
else {
// 1. Let value be the UTF-8 decoding without BOM of body.
value = String::from_utf8_with_replacement_character(body, String::WithBOMHandling::No);
}
// 12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
VERIFY(header.name.has_value() && value.has_value());
// 13. Create an entry with name and value, and append it to entry list.
entry_list.empend(header.name.release_value(), value.release_value());
}
}
}

View file

@ -8,6 +8,9 @@
#pragma once
#include <AK/Forward.h>
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibGC/Ptr.h>
#include <LibJS/Forward.h>
#include <LibWeb/Forward.h>
@ -23,6 +26,24 @@ enum class PackageDataType {
Text,
};
struct MultiPartFormDataHeader {
Optional<String> name;
Optional<String> filename;
Optional<String> content_type;
};
struct ContentDispositionHeader {
String type;
OrderedHashMap<String, String> parameters;
};
struct MultipartParsingError {
String message;
};
template<typename T>
using MultipartParsingErrorOr = ErrorOr<T, MultipartParsingError>;
// https://fetch.spec.whatwg.org/#body-mixin
class BodyMixin {
public:
@ -49,5 +70,6 @@ public:
[[nodiscard]] WebIDL::ExceptionOr<JS::Value> package_data(JS::Realm&, ByteBuffer, PackageDataType, Optional<MimeSniff::MimeType> const&);
[[nodiscard]] WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> consume_body(JS::Realm&, BodyMixin const&, PackageDataType);
[[nodiscard]] MultipartParsingErrorOr<Vector<XHR::FormDataEntry>> parse_multipart_form_data(JS::Realm&, StringView input, MimeSniff::MimeType const& mime_type);
}

View file

@ -37,6 +37,11 @@ constexpr bool is_http_tab_or_space(u32 const code_point)
return code_point == 0x09 || code_point == 0x20;
}
constexpr bool is_http_newline(u32 const code_point)
{
return code_point == 0x0A || code_point == 0x0D;
}
enum class HttpQuotedStringExtractValue {
No,
Yes,

View file

@ -50,6 +50,11 @@ WebIDL::ExceptionOr<GC::Ref<FormData>> FormData::create(JS::Realm& realm, Vector
return construct_impl(realm, move(list));
}
WebIDL::ExceptionOr<GC::Ref<FormData>> FormData::create(JS::Realm& realm, Vector<FormDataEntry> entry_list)
{
return construct_impl(realm, move(entry_list));
}
FormData::FormData(JS::Realm& realm, Vector<FormDataEntry> entry_list)
: PlatformObject(realm)
, m_entry_list(move(entry_list))

View file

@ -28,6 +28,7 @@ public:
static WebIDL::ExceptionOr<GC::Ref<FormData>> construct_impl(JS::Realm&, Vector<FormDataEntry> entry_list);
static WebIDL::ExceptionOr<GC::Ref<FormData>> create(JS::Realm&, Vector<DOMURL::QueryParam> entry_list);
static WebIDL::ExceptionOr<GC::Ref<FormData>> create(JS::Realm&, Vector<FormDataEntry> entry_list);
WebIDL::ExceptionOr<void> append(String const& name, String const& value);
WebIDL::ExceptionOr<void> append(String const& name, GC::Ref<FileAPI::Blob> const& blob_value, Optional<String> const& filename = {});

View file

@ -0,0 +1,8 @@
Harness status: OK
Found 3 tests
3 Pass
Pass Consume empty response.formData() as FormData
Pass Consume empty request.formData() as FormData
Pass Consume multipart/form-data headers case-insensitively

View file

@ -0,0 +1,20 @@
Harness status: OK
Found 14 tests
13 Pass
1 Fail
Pass Consume request's body as text
Pass Consume request's body as blob
Pass Consume request's body as arrayBuffer
Pass Consume request's body as json (error case)
Pass Consume request's body as formData with correct multipart type (error case)
Pass Consume request's body as formData with correct urlencoded type
Pass Consume request's body as formData without correct type (error case)
Pass Consume empty blob request body as arrayBuffer
Pass Consume empty text request body as arrayBuffer
Pass Consume empty blob request body as text
Pass Consume empty text request body as text
Pass Consume empty URLSearchParams request body as text
Fail Consume empty FormData request body as text
Pass Consume empty ArrayBuffer request body as text

View file

@ -0,0 +1,50 @@
Harness status: OK
Found 45 tests
45 Pass
Pass Consume String request's body as text
Pass Consume String request's body as blob
Pass Consume String request's body as arrayBuffer
Pass Consume String request's body as bytes
Pass Consume String request's body as JSON
Pass Consume ArrayBuffer request's body as text
Pass Consume ArrayBuffer request's body as blob
Pass Consume ArrayBuffer request's body as arrayBuffer
Pass Consume ArrayBuffer request's body as bytes
Pass Consume ArrayBuffer request's body as JSON
Pass Consume Uint8Array request's body as text
Pass Consume Uint8Array request's body as blob
Pass Consume Uint8Array request's body as arrayBuffer
Pass Consume Uint8Array request's body as bytes
Pass Consume Uint8Array request's body as JSON
Pass Consume Int8Array request's body as text
Pass Consume Int8Array request's body as blob
Pass Consume Int8Array request's body as arrayBuffer
Pass Consume Int8Array request's body as bytes
Pass Consume Int8Array request's body as JSON
Pass Consume Float32Array request's body as text
Pass Consume Float32Array request's body as blob
Pass Consume Float32Array request's body as arrayBuffer
Pass Consume Float32Array request's body as bytes
Pass Consume Float32Array request's body as JSON
Pass Consume DataView request's body as text
Pass Consume DataView request's body as blob
Pass Consume DataView request's body as arrayBuffer
Pass Consume DataView request's body as bytes
Pass Consume DataView request's body as JSON
Pass Consume FormData request's body as FormData
Pass Consume blob response's body as blob
Pass Consume blob response's body as text
Pass Consume blob response's body as json
Pass Consume blob response's body as arrayBuffer
Pass Consume blob response's body as bytes
Pass Consume blob response's body as blob (empty blob as input)
Pass Consume JSON from text: '"null"'
Pass Consume JSON from text: '"1"'
Pass Consume JSON from text: '"true"'
Pass Consume JSON from text: '"\"string\""'
Pass Trying to consume bad JSON text as JSON: 'undefined'
Pass Trying to consume bad JSON text as JSON: '{'
Pass Trying to consume bad JSON text as JSON: 'a'
Pass Trying to consume bad JSON text as JSON: '['

View file

@ -0,0 +1,20 @@
Harness status: OK
Found 14 tests
13 Pass
1 Fail
Pass Consume response's body as text
Pass Consume response's body as blob
Pass Consume response's body as arrayBuffer
Pass Consume response's body as json (error case)
Pass Consume response's body as formData with correct multipart type (error case)
Pass Consume response's body as formData with correct urlencoded type
Pass Consume response's body as formData without correct type (error case)
Pass Consume empty blob response body as arrayBuffer
Pass Consume empty text response body as arrayBuffer
Pass Consume empty blob response body as text
Pass Consume empty text response body as text
Pass Consume empty URLSearchParams response body as text
Fail Consume empty FormData response body as text
Pass Consume empty ArrayBuffer response body as text

View file

@ -0,0 +1,46 @@
Harness status: Error
Found 40 tests
24 Pass
16 Fail
Pass Consume response's body: from text to text
Pass Consume response's body: from text to blob
Pass Consume response's body: from text to arrayBuffer
Pass Consume response's body: from text to json
Fail Consume response's body: from text with correct multipart type to formData
Fail Consume response's body: from text with correct multipart type to formData with BOM
Pass Consume response's body: from text without correct multipart type to formData (error case)
Pass Consume response's body: from text with correct urlencoded type to formData
Pass Consume response's body: from text without correct urlencoded type to formData (error case)
Fail Consume response's body: from blob to blob
Fail Consume response's body: from blob to text
Fail Consume response's body: from blob to arrayBuffer
Fail Consume response's body: from blob to json
Fail Consume response's body: from blob with correct multipart type to formData
Pass Consume response's body: from blob without correct multipart type to formData (error case)
Fail Consume response's body: from blob with correct urlencoded type to formData
Pass Consume response's body: from blob without correct urlencoded type to formData (error case)
Pass Consume response's body: from FormData to formData
Pass Consume response's body: from FormData without correct type to formData (error case)
Fail Consume response's body: from FormData to blob
Pass Consume response's body: from FormData to text
Pass Consume response's body: from FormData to arrayBuffer
Pass Consume response's body: from URLSearchParams to formData
Pass Consume response's body: from URLSearchParams without correct type to formData (error case)
Fail Consume response's body: from URLSearchParams to blob
Pass Consume response's body: from URLSearchParams to text
Pass Consume response's body: from URLSearchParams to arrayBuffer
Pass Consume response's body: from stream to blob
Pass Consume response's body: from stream to text
Pass Consume response's body: from stream to arrayBuffer
Pass Consume response's body: from stream to json
Fail Consume response's body: from stream with correct multipart type to formData
Pass Consume response's body: from stream without correct multipart type to formData (error case)
Pass Consume response's body: from stream with correct urlencoded type to formData
Pass Consume response's body: from stream without correct urlencoded type to formData (error case)
Fail Consume response's body: from fetch to blob
Fail Consume response's body: from fetch to text
Fail Consume response's body: from fetch to arrayBuffer
Fail Consume response's body: from fetch without correct type to formData (error case)
Fail Consume response's body: from multipart form data blob to formData

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Invalid form data should not crash the browser

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass Ensure capital letters can be used in the boundary value.

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../fetch/api/body/formdata.any.js"></script>

View file

@ -0,0 +1,25 @@
promise_test(async t => {
const res = new Response(new FormData());
const fd = await res.formData();
assert_true(fd instanceof FormData);
}, 'Consume empty response.formData() as FormData');
promise_test(async t => {
const req = new Request('about:blank', {
method: 'POST',
body: new FormData()
});
const fd = await req.formData();
assert_true(fd instanceof FormData);
}, 'Consume empty request.formData() as FormData');
promise_test(async t => {
let formdata = new FormData();
formdata.append('foo', new Blob([JSON.stringify({ bar: "baz", })], { type: "application/json" }));
let blob = await new Response(formdata).blob();
let body = await blob.text();
blob = new Blob([body.toLowerCase()], { type: blob.type.toLowerCase() });
let formdataWithLowercaseBody = await new Response(blob).formData();
assert_true(formdataWithLowercaseBody.has("foo"));
assert_equals(formdataWithLowercaseBody.get("foo").type, "application/json");
}, 'Consume multipart/form-data headers case-insensitively');

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>Request consume empty bodies</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../fetch/api/request/request-consume-empty.any.js"></script>

View file

@ -0,0 +1,89 @@
// META: global=window,worker
// META: title=Request consume empty bodies
function checkBodyText(test, request) {
return request.text().then(function(bodyAsText) {
assert_equals(bodyAsText, "", "Resolved value should be empty");
assert_false(request.bodyUsed);
});
}
async function checkBodyBlob(test, request) {
const bodyAsBlob = await request.blob();
const body = await bodyAsBlob.text();
assert_equals(body, "", "Resolved value should be empty");
assert_false(request.bodyUsed);
}
function checkBodyArrayBuffer(test, request) {
return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
assert_false(request.bodyUsed);
});
}
function checkBodyJSON(test, request) {
return request.json().then(
function(bodyAsJSON) {
assert_unreached("JSON parsing should fail");
},
function() {
assert_false(request.bodyUsed);
});
}
function checkBodyFormData(test, request) {
return request.formData().then(function(bodyAsFormData) {
assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
assert_false(request.bodyUsed);
});
}
function checkBodyFormDataError(test, request) {
return promise_rejects_js(test, TypeError, request.formData()).then(function() {
assert_false(request.bodyUsed);
});
}
function checkRequestWithNoBody(bodyType, checkFunction, headers = []) {
promise_test(function(test) {
var request = new Request("", {"method": "POST", "headers": headers});
assert_false(request.bodyUsed);
return checkFunction(test, request);
}, "Consume request's body as " + bodyType);
}
checkRequestWithNoBody("text", checkBodyText);
checkRequestWithNoBody("blob", checkBodyBlob);
checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer);
checkRequestWithNoBody("json (error case)", checkBodyJSON);
checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
function checkRequestWithEmptyBody(bodyType, body, asText) {
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": body});
assert_false(request.bodyUsed, "bodyUsed is false at init");
if (asText) {
return request.text().then(function(bodyAsString) {
assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
});
}
return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
assert_true(request.bodyUsed, "bodyUsed is true after being consumed");
});
}, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer"));
}
// FIXME: Add BufferSource, FormData and URLSearchParams.
checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
checkRequestWithEmptyBody("text", "", false);
checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
checkRequestWithEmptyBody("text", "", true);
checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950.
checkRequestWithEmptyBody("FormData", new FormData(), true);
checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>Request consume</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../resources/utils.js"></script>
<div id=log></div>
<script src="../../../fetch/api/request/request-consume.any.js"></script>

View file

@ -0,0 +1,148 @@
// META: global=window,worker
// META: title=Request consume
// META: script=../resources/utils.js
function checkBodyText(request, expectedBody) {
return request.text().then(function(bodyAsText) {
assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body");
assert_true(request.bodyUsed, "body as text: bodyUsed turned true");
});
}
async function checkBodyBlob(request, expectedBody, checkContentType) {
const bodyAsBlob = await request.blob();
if (checkContentType)
assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type");
const body = await bodyAsBlob.text();
assert_equals(body, expectedBody, "Retrieve and verify request's body");
assert_true(request.bodyUsed, "body as blob: bodyUsed turned true");
}
function checkBodyArrayBuffer(request, expectedBody) {
return request.arrayBuffer().then(function(bodyAsArrayBuffer) {
validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body");
assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
});
}
function checkBodyBytes(request, expectedBody) {
return request.bytes().then(function(bodyAsUint8Array) {
assert_true(bodyAsUint8Array instanceof Uint8Array);
validateBufferFromString(bodyAsUint8Array.buffer, expectedBody, "Retrieve and verify request's body");
assert_true(request.bodyUsed, "body as bytes: bodyUsed turned true");
});
}
function checkBodyJSON(request, expectedBody) {
return request.json().then(function(bodyAsJSON) {
var strBody = JSON.stringify(bodyAsJSON)
assert_equals(strBody, expectedBody, "Retrieve and verify request's body");
assert_true(request.bodyUsed, "body as json: bodyUsed turned true");
});
}
function checkBodyFormData(request, expectedBody) {
return request.formData().then(function(bodyAsFormData) {
assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
assert_true(request.bodyUsed, "body as formData: bodyUsed turned true");
});
}
function checkRequestBody(body, expected, bodyType) {
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] });
assert_false(request.bodyUsed, "bodyUsed is false at init");
return checkBodyText(request, expected);
}, "Consume " + bodyType + " request's body as text");
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": body });
assert_false(request.bodyUsed, "bodyUsed is false at init");
return checkBodyBlob(request, expected);
}, "Consume " + bodyType + " request's body as blob");
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": body });
assert_false(request.bodyUsed, "bodyUsed is false at init");
return checkBodyArrayBuffer(request, expected);
}, "Consume " + bodyType + " request's body as arrayBuffer");
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": body });
assert_false(request.bodyUsed, "bodyUsed is false at init");
return checkBodyBytes(request, expected);
}, "Consume " + bodyType + " request's body as bytes");
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": body });
assert_false(request.bodyUsed, "bodyUsed is false at init");
return checkBodyJSON(request, expected);
}, "Consume " + bodyType + " request's body as JSON");
}
var textData = JSON.stringify("This is response's body");
var blob = new Blob([textData], { "type" : "text/plain" });
checkRequestBody(textData, textData, "String");
var string = "\"123456\"";
function getArrayBuffer() {
var arrayBuffer = new ArrayBuffer(8);
var int8Array = new Int8Array(arrayBuffer);
for (var cptr = 0; cptr < 8; cptr++)
int8Array[cptr] = string.charCodeAt(cptr);
return arrayBuffer;
}
function getArrayBufferWithZeros() {
var arrayBuffer = new ArrayBuffer(10);
var int8Array = new Int8Array(arrayBuffer);
for (var cptr = 0; cptr < 8; cptr++)
int8Array[cptr + 1] = string.charCodeAt(cptr);
return arrayBuffer;
}
checkRequestBody(getArrayBuffer(), string, "ArrayBuffer");
checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array");
checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array");
checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array");
checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView");
promise_test(function(test) {
var formData = new FormData();
formData.append("name", "value")
var request = new Request("", {"method": "POST", "body": formData });
assert_false(request.bodyUsed, "bodyUsed is false at init");
return checkBodyFormData(request, formData);
}, "Consume FormData request's body as FormData");
function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) {
promise_test(function(test) {
var response = new Response(blobBody);
assert_false(response.bodyUsed, "bodyUsed is false at init");
return checkFunction(response, blobData);
}, "Consume blob response's body as " + bodyType);
}
checkBlobResponseBody(blob, textData, "blob", checkBodyBlob);
checkBlobResponseBody(blob, textData, "text", checkBodyText);
checkBlobResponseBody(blob, textData, "json", checkBodyJSON);
checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer);
checkBlobResponseBody(blob, textData, "bytes", checkBodyBytes);
checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob);
var goodJSONValues = ["null", "1", "true", "\"string\""];
goodJSONValues.forEach(function(value) {
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": value});
return request.json().then(function(v) {
assert_equals(v, JSON.parse(value));
});
}, "Consume JSON from text: '" + JSON.stringify(value) + "'");
});
var badJSONValues = ["undefined", "{", "a", "["];
badJSONValues.forEach(function(value) {
promise_test(function(test) {
var request = new Request("", {"method": "POST", "body": value});
return promise_rejects_js(test, SyntaxError, request.json());
}, "Trying to consume bad JSON text as JSON: '" + value + "'");
});

View file

@ -0,0 +1,120 @@
var RESOURCES_DIR = "../resources/";
function dirname(path) {
return path.replace(/\/[^\/]*$/, '/')
}
function checkRequest(request, ExpectedValuesDict) {
for (var attribute in ExpectedValuesDict) {
switch(attribute) {
case "headers":
for (var key in ExpectedValuesDict["headers"].keys()) {
assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key),
"Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key));
}
break;
case "body":
//for checking body's content, a dedicated asyncronous/promise test should be used
assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header")
break;
case "method":
case "referrer":
case "referrerPolicy":
case "credentials":
case "cache":
case "redirect":
case "integrity":
case "url":
case "destination":
assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute")
break;
default:
break;
}
}
}
function stringToArray(str) {
var array = new Uint8Array(str.length);
for (var i=0, strLen = str.length; i < strLen; i++)
array[i] = str.charCodeAt(i);
return array;
}
function encode_utf8(str)
{
if (self.TextEncoder)
return (new TextEncoder).encode(str);
return stringToArray(unescape(encodeURIComponent(str)));
}
function validateBufferFromString(buffer, expectedValue, message)
{
return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message);
}
function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) {
// Passing Uint8Array for byte streams; non-byte streams will simply ignore it
return reader.read(new Uint8Array(64)).then(function(data) {
if (!data.done) {
assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
var newBuffer;
if (retrievedArrayBuffer) {
newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
newBuffer.set(retrievedArrayBuffer, 0);
newBuffer.set(data.value, retrievedArrayBuffer.length);
} else {
newBuffer = data.value;
}
return validateStreamFromString(reader, expectedValue, newBuffer);
}
validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream");
});
}
function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) {
// Passing Uint8Array for byte streams; non-byte streams will simply ignore it
return reader.read(new Uint8Array(64)).then(function(data) {
if (!data.done) {
assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array");
var newBuffer;
if (retrievedArrayBuffer) {
newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length);
newBuffer.set(retrievedArrayBuffer, 0);
newBuffer.set(data.value, retrievedArrayBuffer.length);
} else {
newBuffer = data.value;
}
return validateStreamFromPartialString(reader, expectedValue, newBuffer);
}
var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer);
return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream");
});
}
// From streams tests
function delay(milliseconds)
{
return new Promise(function(resolve) {
step_timeout(resolve, milliseconds);
});
}
function requestForbiddenHeaders(desc, forbiddenHeaders) {
var url = RESOURCES_DIR + "inspect-headers.py";
var requestInit = {"headers": forbiddenHeaders}
var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|");
promise_test(function(test){
return fetch(url + urlParameters, requestInit).then(function(resp) {
assert_equals(resp.status, 200, "HTTP status is 200");
assert_equals(resp.type , "basic", "Response's type is basic");
for (var header in forbiddenHeaders)
assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined");
});
}, desc);
}

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>Response consume empty bodies</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../fetch/api/response/response-consume-empty.any.js"></script>

View file

@ -0,0 +1,88 @@
// META: global=window,worker
// META: title=Response consume empty bodies
function checkBodyText(test, response) {
return response.text().then(function(bodyAsText) {
assert_equals(bodyAsText, "", "Resolved value should be empty");
assert_false(response.bodyUsed);
});
}
async function checkBodyBlob(test, response) {
const bodyAsBlob = await response.blob();
const body = await bodyAsBlob.text();
assert_equals(body, "", "Resolved value should be empty");
assert_false(response.bodyUsed);
}
function checkBodyArrayBuffer(test, response) {
return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
assert_false(response.bodyUsed);
});
}
function checkBodyJSON(test, response) {
return response.json().then(
function(bodyAsJSON) {
assert_unreached("JSON parsing should fail");
},
function() {
assert_false(response.bodyUsed);
});
}
function checkBodyFormData(test, response) {
return response.formData().then(function(bodyAsFormData) {
assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
assert_false(response.bodyUsed);
});
}
function checkBodyFormDataError(test, response) {
return promise_rejects_js(test, TypeError, response.formData()).then(function() {
assert_false(response.bodyUsed);
});
}
function checkResponseWithNoBody(bodyType, checkFunction, headers = []) {
promise_test(function(test) {
var response = new Response(undefined, { "headers": headers });
assert_false(response.bodyUsed);
return checkFunction(test, response);
}, "Consume response's body as " + bodyType);
}
checkResponseWithNoBody("text", checkBodyText);
checkResponseWithNoBody("blob", checkBodyBlob);
checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer);
checkResponseWithNoBody("json (error case)", checkBodyJSON);
checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]);
checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]);
checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError);
function checkResponseWithEmptyBody(bodyType, body, asText) {
promise_test(function(test) {
var response = new Response(body);
assert_false(response.bodyUsed, "bodyUsed is false at init");
if (asText) {
return response.text().then(function(bodyAsString) {
assert_equals(bodyAsString.length, 0, "Resolved value should be empty");
assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
});
}
return response.arrayBuffer().then(function(bodyAsArrayBuffer) {
assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty");
assert_true(response.bodyUsed, "bodyUsed is true after being consumed");
});
}, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer"));
}
checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false);
checkResponseWithEmptyBody("text", "", false);
checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true);
checkResponseWithEmptyBody("text", "", true);
checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true);
checkResponseWithEmptyBody("FormData", new FormData(), true);
checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true);

View file

@ -0,0 +1,317 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Response consume</title>
<meta name="help" href="https://fetch.spec.whatwg.org/#response">
<meta name="help" href="https://fetch.spec.whatwg.org/#body-mixin">
<meta name="author" title="Canon Research France" href="https://www.crf.canon.fr">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../resources/utils.js"></script>
</head>
<body>
<script>
function blobToFormDataResponse(name, blob) {
var formData = new FormData();
formData.append(name, blob);
return new Response(formData);
}
function readBlobAsArrayBuffer(blob) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function(evt) {
resolve(reader.result);
};
reader.onerror = function(evt) {
reject("Blob's reader failed");
};
reader.readAsArrayBuffer(blob);
});
}
function blobToTypeViaFetch(blob) {
var url = URL.createObjectURL(blob);
return fetch(url).then(function(response) {
return response.headers.get('Content-Type');
});
}
function responsePromise(body, responseInit) {
return new Promise(function(resolve, reject) {
resolve(new Response(body, responseInit));
});
}
function responseStringToMultipartFormTextData(response, name, value) {
assert_true(response.headers.has("Content-Type"), "Response contains Content-Type header");
var boundaryMatches = response.headers.get("Content-Type").match(/;\s*boundary=("?)([^";\s]*)\1/);
assert_true(!!boundaryMatches, "Response contains boundary parameter");
return stringToMultipartFormTextData(boundaryMatches[2], name, value);
}
function streamResponsePromise(streamData, responseInit) {
return new Promise(function(resolve, reject) {
var stream = new ReadableStream({
start: function(controller) {
controller.enqueue(stringToArray(streamData));
controller.close();
}
});
resolve(new Response(stream, responseInit));
});
}
function stringToMultipartFormTextData(multipartBoundary, name, value) {
return ('--' + multipartBoundary + '\r\n' +
'Content-Disposition: form-data;name="' + name + '"\r\n' +
'\r\n' +
value + '\r\n' +
'--' + multipartBoundary + '--\r\n');
}
function checkBodyText(test, response, expectedBody) {
return response.text().then( function(bodyAsText) {
assert_equals(bodyAsText, expectedBody, "Retrieve and verify response's body");
assert_true(response.bodyUsed, "body as text: bodyUsed turned true");
});
}
function checkBodyBlob(test, response, expectedBody, expectedType) {
return response.blob().then(function(bodyAsBlob) {
assert_equals(bodyAsBlob.type, expectedType || "text/plain", "Blob body type should be computed from the response Content-Type");
var promise = blobToTypeViaFetch(bodyAsBlob).then(function(type) {
assert_equals(type, expectedType || "text/plain", 'Type via blob URL');
return new Promise( function (resolve, reject) {
var reader = new FileReader();
reader.onload = function(evt) {
resolve(reader.result)
};
reader.onerror = function () {
reject("Blob's reader failed");
};
reader.readAsText(bodyAsBlob);
});
});
return promise.then(function(body) {
assert_equals(body, expectedBody, "Retrieve and verify response's body");
assert_true(response.bodyUsed, "body as blob: bodyUsed turned true");
});
});
}
function checkBodyArrayBuffer(test, response, expectedBody) {
return response.arrayBuffer().then( function(bodyAsArrayBuffer) {
validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify response's body");
assert_true(response.bodyUsed, "body as arrayBuffer: bodyUsed turned true");
});
}
function checkBodyJSON(test, response, expectedBody) {
return response.json().then(function(bodyAsJSON) {
var strBody = JSON.stringify(bodyAsJSON)
assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
assert_true(response.bodyUsed, "body as json: bodyUsed turned true");
});
}
function checkBodyFormDataMultipart(test, response, expectedBody) {
return response.formData().then(function(bodyAsFormData) {
assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
var entryName = "name";
var strBody = responseStringToMultipartFormTextData(response, entryName, bodyAsFormData.get(entryName));
assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
});
}
function checkBodyFormDataUrlencoded(test, response, expectedBody) {
return response.formData().then(function(bodyAsFormData) {
assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData");
var entryName = "name";
var strBody = entryName + "=" + bodyAsFormData.get(entryName);
assert_equals(strBody, expectedBody, "Retrieve and verify response's body");
assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
});
}
function checkBodyFormDataError(test, response, expectedBody) {
return promise_rejects_js(test, TypeError, response.formData()).then(function() {
assert_true(response.bodyUsed, "body as formData: bodyUsed turned true");
});
}
function checkResponseBody(responsePromise, expectedBody, checkFunction, bodyTypes) {
promise_test(function(test) {
return responsePromise.then(function(response) {
assert_false(response.bodyUsed, "bodyUsed is false at init");
return checkFunction(test, response, expectedBody);
});
}, "Consume response's body: " + bodyTypes);
}
var textData = JSON.stringify("This is response's body");
var textResponseInit = { "headers": [["Content-Type", "text/PLAIN"]] };
var blob = new Blob([textData], { "type": "application/octet-stream" });
var multipartBoundary = "boundary-" + Math.random();
var formData = new FormData();
var formTextResponseInit = { "headers": [["Content-Type", 'multipart/FORM-data; boundary="' + multipartBoundary + '"']] };
var formTextData = stringToMultipartFormTextData(multipartBoundary, "name", textData);
var formBlob = new Blob([formTextData]);
var urlSearchParamsData = "name=value";
var urlSearchParams = new URLSearchParams(urlSearchParamsData);
var urlSearchParamsType = "application/x-www-form-urlencoded;charset=UTF-8";
var urlSearchParamsResponseInit = { "headers": [["Content-Type", urlSearchParamsType]] };
var urlSearchParamsBlob = new Blob([urlSearchParamsData], { "type": urlSearchParamsType });
formData.append("name", textData);
// https://fetch.spec.whatwg.org/#concept-body-package-data
// "UTF-8 decoded without BOM" is used for formData(), either in
// "multipart/form-data" and "application/x-www-form-urlencoded" cases,
// so BOMs in the values should be kept.
// (The "application/x-www-form-urlencoded" cases are tested in
// url/urlencoded-parser.any.js)
var textDataWithBom = "\uFEFFquick\uFEFFfox\uFEFF";
var formTextDataWithBom = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom);
var formTextDataWithBomExpectedForMultipartFormData = stringToMultipartFormTextData(multipartBoundary, "name", textDataWithBom);
checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyText, "from text to text");
checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyBlob, "from text to blob");
checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyArrayBuffer, "from text to arrayBuffer");
checkResponseBody(responsePromise(textData, textResponseInit), textData, checkBodyJSON, "from text to json");
checkResponseBody(responsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from text with correct multipart type to formData");
checkResponseBody(responsePromise(formTextDataWithBom, formTextResponseInit), formTextDataWithBomExpectedForMultipartFormData, checkBodyFormDataMultipart, "from text with correct multipart type to formData with BOM");
checkResponseBody(responsePromise(formTextData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct multipart type to formData (error case)");
checkResponseBody(responsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from text with correct urlencoded type to formData");
checkResponseBody(responsePromise(urlSearchParamsData, textResponseInit), undefined, checkBodyFormDataError, "from text without correct urlencoded type to formData (error case)");
checkResponseBody(responsePromise(blob, textResponseInit), textData, checkBodyBlob, "from blob to blob");
checkResponseBody(responsePromise(blob), textData, checkBodyText, "from blob to text");
checkResponseBody(responsePromise(blob), textData, checkBodyArrayBuffer, "from blob to arrayBuffer");
checkResponseBody(responsePromise(blob), textData, checkBodyJSON, "from blob to json");
checkResponseBody(responsePromise(formBlob, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from blob with correct multipart type to formData");
checkResponseBody(responsePromise(formBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct multipart type to formData (error case)");
checkResponseBody(responsePromise(urlSearchParamsBlob, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from blob with correct urlencoded type to formData");
checkResponseBody(responsePromise(urlSearchParamsBlob, textResponseInit), undefined, checkBodyFormDataError, "from blob without correct urlencoded type to formData (error case)");
function checkFormDataResponseBody(responsePromise, expectedName, expectedValue, checkFunction, bodyTypes) {
promise_test(function(test) {
return responsePromise.then(function(response) {
assert_false(response.bodyUsed, "bodyUsed is false at init");
var expectedBody = responseStringToMultipartFormTextData(response, expectedName, expectedValue);
return Promise.resolve().then(function() {
if (checkFunction === checkBodyFormDataMultipart)
return expectedBody;
// Modify expectedBody to use the same spacing for
// Content-Disposition parameters as Response and FormData does.
var response2 = new Response(formData);
return response2.text().then(function(formDataAsText) {
var reName = /[ \t]*;[ \t]*name=/;
var nameMatches = formDataAsText.match(reName);
return expectedBody.replace(reName, nameMatches[0]);
});
}).then(function(expectedBody) {
return checkFunction(test, response, expectedBody);
});
});
}, "Consume response's body: " + bodyTypes);
}
checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyFormDataMultipart, "from FormData to formData");
checkResponseBody(responsePromise(formData, textResponseInit), undefined, checkBodyFormDataError, "from FormData without correct type to formData (error case)");
checkFormDataResponseBody(responsePromise(formData), "name", textData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, response.headers.get('Content-Type').toLowerCase()); }, "from FormData to blob");
checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyText, "from FormData to text");
checkFormDataResponseBody(responsePromise(formData), "name", textData, checkBodyArrayBuffer, "from FormData to arrayBuffer");
checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyFormDataUrlencoded, "from URLSearchParams to formData");
checkResponseBody(responsePromise(urlSearchParams, textResponseInit), urlSearchParamsData, checkBodyFormDataError, "from URLSearchParams without correct type to formData (error case)");
checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, function(test, response, expectedBody) { return checkBodyBlob(test, response, expectedBody, "application/x-www-form-urlencoded;charset=utf-8"); }, "from URLSearchParams to blob");
checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyText, "from URLSearchParams to text");
checkResponseBody(responsePromise(urlSearchParams), urlSearchParamsData, checkBodyArrayBuffer, "from URLSearchParams to arrayBuffer");
checkResponseBody(streamResponsePromise(textData, textResponseInit), textData, checkBodyBlob, "from stream to blob");
checkResponseBody(streamResponsePromise(textData), textData, checkBodyText, "from stream to text");
checkResponseBody(streamResponsePromise(textData), textData, checkBodyArrayBuffer, "from stream to arrayBuffer");
checkResponseBody(streamResponsePromise(textData), textData, checkBodyJSON, "from stream to json");
checkResponseBody(streamResponsePromise(formTextData, formTextResponseInit), formTextData, checkBodyFormDataMultipart, "from stream with correct multipart type to formData");
checkResponseBody(streamResponsePromise(formTextData), formTextData, checkBodyFormDataError, "from stream without correct multipart type to formData (error case)");
checkResponseBody(streamResponsePromise(urlSearchParamsData, urlSearchParamsResponseInit), urlSearchParamsData, checkBodyFormDataUrlencoded, "from stream with correct urlencoded type to formData");
checkResponseBody(streamResponsePromise(urlSearchParamsData), urlSearchParamsData, checkBodyFormDataError, "from stream without correct urlencoded type to formData (error case)");
checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyBlob, "from fetch to blob");
checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyText, "from fetch to text");
checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyArrayBuffer, "from fetch to arrayBuffer");
checkResponseBody(fetch("../resources/top.txt"), "top", checkBodyFormDataError, "from fetch without correct type to formData (error case)");
promise_test(function(test) {
var response = new Response(new Blob([
"--boundary\r\n",
"Content-Disposition: form-data; name=string\r\n",
"\r\nvalue", new Uint8Array([0xC2, 0xA0]), "1\r\n",
"--boundary\r\n",
"Content-Disposition: form-data; name=string-with-default-charset\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"\r\nvalue", new Uint8Array([0xC2, 0xA0]), "2\r\n",
"--boundary\r\n",
"Content-Disposition: form-data; name=string-with-non-default-charset\r\n",
"Content-Type: text/plain; charset=iso-8859-1\r\n",
"\r\nvalue", new Uint8Array([0xC2, 0xA0]), "3\r\n",
"--boundary\r\n",
"Content-Disposition: form-data; name=string-with-non-default-type\r\n",
"Content-Type: application/octet-stream\r\n",
"\r\nvalue", new Uint8Array([0xC2, 0xA0]), "4\r\n",
"--boundary\r\n",
"Content-Disposition: form-data; name=file; filename=file1\r\n",
"Content-Type: application/octet-stream; x-param=x-value\r\n",
"\r\n", new Uint8Array([5, 0x0, 0xFF]), "\r\n",
"--boundary\r\n",
"Content-Disposition: form-data; name=\"file-without-type\"; filename=\"file2\"\r\n",
"\r\n", new Uint8Array([6, 0x0, 0x7F, 0xFF]), "\r\n",
"--boundary--\r\n"
]), { "headers": [["Content-Type", 'multipart/form-data; boundary="boundary"']] });
return response.formData().then(function(bodyAsFormData) {
// Non-file parts must always be decoded using utf-8 encoding.
assert_equals(bodyAsFormData.get("string"), "value\u00A01", "Retrieve and verify response's 1st entry value");
assert_equals(bodyAsFormData.get("string-with-default-charset"), "value\u00A02", "Retrieve and verify response's 2nd entry value");
assert_equals(bodyAsFormData.get("string-with-non-default-charset"), "value\u00A03", "Retrieve and verify response's 3rd entry value");
assert_equals(bodyAsFormData.get("string-with-non-default-type"), "value\u00A04", "Retrieve and verify response's 4th entry value");
// The name of a File must be taken from the filename parameter in
// the Content-Disposition header field.
assert_equals(bodyAsFormData.get("file").name, "file1", "Retrieve and verify response's 5th entry name property");
assert_equals(bodyAsFormData.get("file-without-type").name, "file2", "Retrieve and verify response's 6th entry name property");
// The type of a File must be taken from the Content-Type header field
// which defaults to "text/plain".
assert_equals(bodyAsFormData.get("file").type, "application/octet-stream; x-param=x-value", "Retrieve and verify response's 5th entry type property");
assert_equals(bodyAsFormData.get("file-without-type").type, "text/plain", "Retrieve and verify response's 6th entry type property");
return Promise.resolve().then(function() {
return blobToFormDataResponse("file", bodyAsFormData.get("file")).text().then(function(bodyAsText) {
// Verify that filename, name and type are preserved.
assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file1\2[;\r]/i, "Retrieve and verify response's 5th entry filename parameter");
assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file\2[;\r]/i, "Retrieve and verify response's 5th entry name parameter");
assert_regexp_match(bodyAsText, /\r\nContent-Type: *application\/octet-stream; x-param=x-value\r\n/i, "Retrieve and verify response's 5th entry type field");
// Verify that the content is preserved.
return readBlobAsArrayBuffer(bodyAsFormData.get("file")).then(function(arrayBuffer) {
assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([5, 0x0, 0xFF]), "Retrieve and verify response's 5th entry content");
});
});
}).then(function() {
return blobToFormDataResponse("file-without-type", bodyAsFormData.get("file-without-type")).text().then(function(bodyAsText) {
// Verify that filename, name and type are preserved.
assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *filename=("?)file2\2[;\r]/i, "Retrieve and verify response's 6th entry filename parameter");
assert_regexp_match(bodyAsText, /\r\nContent-Disposition: *form-data;([^\r\n]*;)* *name=("?)file-without-type\2[;\r]/i, "Retrieve and verify response's 6th entry name parameter");
assert_regexp_match(bodyAsText, /\r\nContent-Type: *text\/plain\r\n/i, "Retrieve and verify response's 6th entry type field");
// Verify that the content is preserved.
return readBlobAsArrayBuffer(bodyAsFormData.get("file-without-type")).then(function(arrayBuffer) {
assert_array_equals(new Uint8Array(arrayBuffer), new Uint8Array([6, 0x0, 0x7F, 0xFF]), "Retrieve and verify response's 6th entry content");
});
});
});
});
}, "Consume response's body: from multipart form data blob to formData");
</script>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../fetch/content-type/multipart-malformed.any.js"></script>

View file

@ -0,0 +1,22 @@
// This is a repro for Chromium issue https://crbug.com/1412007.
promise_test(t => {
const form_string =
"--Boundary_with_capital_letters\r\n" +
"Content-Type: application/json\r\n" +
'Content-Disposition: form-data; name="does_this_work"\r\n' +
"\r\n" +
'YES\r\n' +
"--Boundary_with_capital_letters-Random junk";
const r = new Response(new Blob([form_string]), {
headers: [
[
"Content-Type",
"multipart/form-data; boundary=Boundary_with_capital_letters",
],
],
});
return promise_rejects_js(t, TypeError, r.formData(),
"form data should fail to parse");
}, "Invalid form data should not crash the browser");

View file

@ -0,0 +1,8 @@
<!doctype html>
<meta charset=utf-8>
<title>Ensure capital letters can be used in the boundary value.</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../fetch/content-type/multipart.window.js"></script>

View file

@ -0,0 +1,33 @@
// META: title=Ensure capital letters can be used in the boundary value.
setup({ single_test: true });
(async () => {
const form_string =
"--Boundary_with_capital_letters\r\n" +
"Content-Type: application/json\r\n" +
'Content-Disposition: form-data; name="does_this_work"\r\n' +
"\r\n" +
'YES\r\n' +
"--Boundary_with_capital_letters--\r\n";
const r = new Response(new Blob([form_string]), {
headers: [
[
"Content-Type",
"multipart/form-data; boundary=Boundary_with_capital_letters",
],
],
});
var s = "";
try {
const fd = await r.formData();
for (const [key, value] of fd.entries()) {
s += (`${key} = ${value}`);
}
} catch (ex) {
s = ex;
}
assert_equals(s, "does_this_work = YES");
done();
})();