Browse Source

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

Feng Yu 6 months ago
parent
commit
ef339252cf
26 changed files with 1355 additions and 4 deletions
  1. 241 4
      Libraries/LibWeb/Fetch/Body.cpp
  2. 22 0
      Libraries/LibWeb/Fetch/Body.h
  3. 5 0
      Libraries/LibWeb/Fetch/Infrastructure/HTTP.h
  4. 5 0
      Libraries/LibWeb/XHR/FormData.cpp
  5. 1 0
      Libraries/LibWeb/XHR/FormData.h
  6. 8 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/api/body/formdata.any.txt
  7. 20 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume-empty.any.txt
  8. 50 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume.any.txt
  9. 20 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume-empty.any.txt
  10. 46 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume.txt
  11. 6 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart-malformed.any.txt
  12. 6 0
      Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart.window.txt
  13. 15 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.html
  14. 25 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.js
  15. 15 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.html
  16. 89 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.js
  17. 15 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.html
  18. 148 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.js
  19. 120 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/resources/utils.js
  20. 15 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.html
  21. 88 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.js
  22. 317 0
      Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume.html
  23. 15 0
      Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.html
  24. 22 0
      Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.js
  25. 8 0
      Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.html
  26. 33 0
      Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.js

+ 241 - 4
Libraries/LibWeb/Fetch/Body.cpp

@@ -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 mimeType’s 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 mimeType’s 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: mimeType’s essence is "multipart/form-data".
+    VERIFY(mime_type.essence() == "multipart/form-data"sv);
+
+    // 2. If mimeType’s parameters["boundary"] does not exist, return failure. Otherwise, let boundary be the result of UTF-8 decoding mimeType’s 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());
+    }
+}
+
 }

+ 22 - 0
Libraries/LibWeb/Fetch/Body.h

@@ -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);
 
 }

+ 5 - 0
Libraries/LibWeb/Fetch/Infrastructure/HTTP.h

@@ -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,

+ 5 - 0
Libraries/LibWeb/XHR/FormData.cpp

@@ -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))

+ 1 - 0
Libraries/LibWeb/XHR/FormData.h

@@ -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 = {});

+ 8 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/api/body/formdata.any.txt

@@ -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

+ 20 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume-empty.any.txt

@@ -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

+ 50 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume.any.txt

@@ -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: '['

+ 20 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume-empty.any.txt

@@ -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

+ 46 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume.txt

@@ -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

+ 6 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart-malformed.any.txt

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

+ 6 - 0
Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart.window.txt

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

+ 15 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.html

@@ -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>

+ 25 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.js

@@ -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');

+ 15 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.html

@@ -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>

+ 89 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.js

@@ -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);

+ 15 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.html

@@ -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>

+ 148 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.js

@@ -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 + "'");
+});

+ 120 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/resources/utils.js

@@ -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);
+}

+ 15 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.html

@@ -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>

+ 88 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.js

@@ -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);

+ 317 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume.html

@@ -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>

+ 15 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.html

@@ -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>

+ 22 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.js

@@ -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");

+ 8 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.html

@@ -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>

+ 33 - 0
Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.js

@@ -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();
+})();