LibWeb: Store HTTP methods and headers as ByteString

The spec declares these as a byte sequence, which we then implemented as
a ByteBuffer. This has become pretty awkward to deal with, as evidenced
by the plethora of `MUST(ByteBuffer::copy(...))` and `.bytes()` calls
everywhere inside Fetch. We would then treat the bytes as a string
anyways by wrapping them in StringView everywhere.

We now store these as a ByteString. This is more comfortable to deal
with, and we no longer need to continually copy underlying storage (as
ByteString is ref-counted).

This work is largely preparatory for an upcoming HTTP header refactor.
This commit is contained in:
Timothy Flynn 2025-11-24 18:35:55 -05:00 committed by Tim Flynn
parent ed27eea091
commit f675cfe90f
Notes: github-actions[bot] 2025-11-26 14:16:12 +00:00
28 changed files with 480 additions and 651 deletions

View file

@ -21,7 +21,7 @@ namespace Web::ContentSecurityPolicy {
GC_DEFINE_ALLOCATOR(Policy);
// https://w3c.github.io/webappsec-csp/#abstract-opdef-parse-a-serialized-csp
GC::Ref<Policy> Policy::parse_a_serialized_csp(GC::Heap& heap, Variant<ByteBuffer, String> serialized, Source source, Disposition disposition)
GC::Ref<Policy> Policy::parse_a_serialized_csp(GC::Heap& heap, Variant<ByteString, String> serialized, Source source, Disposition disposition)
{
// To parse a serialized CSP, given a byte sequence or string serialized, a source source, and a disposition disposition,
// execute the following steps.
@ -31,7 +31,7 @@ GC::Ref<Policy> Policy::parse_a_serialized_csp(GC::Heap& heap, Variant<ByteBuffe
// 1. If serialized is a byte sequence, then set serialized to be the result of isomorphic decoding serialized.
auto serialized_string = serialized.has<String>()
? serialized.get<String>()
: Infra::isomorphic_decode(serialized.get<ByteBuffer>());
: Infra::isomorphic_decode(serialized.get<ByteString>());
// 2. Let policy be a new policy with an empty directive set, a source of source, and a disposition of disposition.
auto policy = heap.allocate<Policy>();
@ -101,9 +101,9 @@ GC::Ref<PolicyList> Policy::parse_a_responses_content_security_policies(GC::Heap
// 2. For each token returned by extracting header list values given Content-Security-Policy and responses header
// list:
auto enforce_policy_tokens_or_failure = response->header_list()->extract_header_list_values("Content-Security-Policy"sv.bytes());
auto enforce_policy_tokens_or_failure = response->header_list()->extract_header_list_values("Content-Security-Policy"sv);
if (auto const* enforce_policy_tokens = enforce_policy_tokens_or_failure.get_pointer<Vector<ByteBuffer>>()) {
if (auto const* enforce_policy_tokens = enforce_policy_tokens_or_failure.get_pointer<Vector<ByteString>>()) {
for (auto const& enforce_policy_token : *enforce_policy_tokens) {
// 1. Let policy be the result of parsing token, with a source of "header", and a disposition of "enforce".
auto policy = parse_a_serialized_csp(heap, enforce_policy_token, Policy::Source::Header, Policy::Disposition::Enforce);
@ -116,9 +116,9 @@ GC::Ref<PolicyList> Policy::parse_a_responses_content_security_policies(GC::Heap
// 3. For each token returned by extracting header list values given Content-Security-Policy-Report-Only and
// responses header list:
auto report_policy_tokens_or_failure = response->header_list()->extract_header_list_values("Content-Security-Policy-Report-Only"sv.bytes());
auto report_policy_tokens_or_failure = response->header_list()->extract_header_list_values("Content-Security-Policy-Report-Only"sv);
if (auto const* report_policy_tokens = enforce_policy_tokens_or_failure.get_pointer<Vector<ByteBuffer>>()) {
if (auto const* report_policy_tokens = enforce_policy_tokens_or_failure.get_pointer<Vector<ByteString>>()) {
for (auto const& report_policy_token : *report_policy_tokens) {
// 1. Let policy be the result of parsing token, with a source of "header", and a disposition of "report".
auto policy = parse_a_serialized_csp(heap, report_policy_token, Policy::Source::Header, Policy::Disposition::Report);

View file

@ -38,7 +38,7 @@ public:
~Policy() = default;
[[nodiscard]] static GC::Ref<Policy> parse_a_serialized_csp(GC::Heap&, Variant<ByteBuffer, String> serialized, Source source, Disposition disposition);
[[nodiscard]] static GC::Ref<Policy> parse_a_serialized_csp(GC::Heap&, Variant<ByteString, String> serialized, Source source, Disposition disposition);
[[nodiscard]] static GC::Ref<PolicyList> parse_a_responses_content_security_policies(GC::Heap&, GC::Ref<Fetch::Infrastructure::Response const> response);
[[nodiscard]] static GC::Ref<Policy> create_from_serialized_policy(GC::Heap&, SerializedPolicy const&);

View file

@ -29,7 +29,7 @@ GC_DEFINE_ALLOCATOR(Violation);
Violation::Violation(GC::Ptr<JS::Object> global_object, GC::Ref<Policy const> policy, String directive)
: m_global_object(global_object)
, m_policy(policy)
, m_effective_directive(directive)
, m_effective_directive(move(directive))
{
}
@ -38,7 +38,7 @@ GC::Ref<Violation> Violation::create_a_violation_object_for_global_policy_and_di
{
// 1. Let violation be a new violation whose global object is global, policy is policy, effective directive is
// directive, and resource is null.
auto violation = realm.create<Violation>(global_object, policy, directive);
auto violation = realm.create<Violation>(global_object, policy, move(directive));
// FIXME: 2. If the user agent is currently executing script, and can extract a source files URL, line number,
// and column number from the global, set violations source file, line number, and column number
@ -406,7 +406,7 @@ void Violation::report_a_violation(JS::Realm& realm)
// method
// "POST"
request->set_method(MUST(ByteBuffer::copy("POST"sv.bytes())));
request->set_method("POST"sv);
// url
// violations url
@ -448,8 +448,7 @@ void Violation::report_a_violation(JS::Realm& realm)
// A header list containing a single header whose name is "Content-Type", and value is
// "application/csp-report"
auto header_list = Fetch::Infrastructure::HeaderList::create(vm);
auto content_type_header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "application/csp-report"sv);
header_list->append(move(content_type_header));
header_list->append({ "Content-Type"sv, "application/csp-report"sv });
request->set_header_list(header_list);
// body

View file

@ -385,15 +385,15 @@ WebIDL::ExceptionOr<GC::Ref<Document>> Document::create_and_initialize(Type type
document->m_window = window;
// NOTE: Non-standard: Pull out the Last-Modified header for use in the lastModified property.
if (auto maybe_last_modified = navigation_params.response->header_list()->get("Last-Modified"sv.bytes()); maybe_last_modified.has_value()) {
if (auto maybe_last_modified = navigation_params.response->header_list()->get("Last-Modified"sv); maybe_last_modified.has_value()) {
// rfc9110, 8.8.2: The Last-Modified header field must be in GMT time zone.
// document->m_last_modified is in local time zone.
document->m_last_modified = AK::UnixDateTime::parse("%a, %d %b %Y %H:%M:%S GMT"sv, maybe_last_modified.value(), true);
}
// NOTE: Non-standard: Pull out the Content-Language header to determine the document's language.
if (auto maybe_http_content_language = navigation_params.response->header_list()->get("Content-Language"sv.bytes()); maybe_http_content_language.has_value()) {
if (auto maybe_content_language = String::from_utf8(maybe_http_content_language.value()); !maybe_content_language.is_error())
if (auto maybe_http_content_language = navigation_params.response->header_list()->get("Content-Language"sv); maybe_http_content_language.has_value()) {
if (auto maybe_content_language = String::from_byte_string(maybe_http_content_language.value()); !maybe_content_language.is_error())
document->m_http_content_language = maybe_content_language.release_value();
}
@ -423,7 +423,7 @@ WebIDL::ExceptionOr<GC::Ref<Document>> Document::create_and_initialize(Type type
// navigationParams's response's service worker timing info.
// 15. If navigationParams's response has a `Refresh` header, then:
if (auto maybe_refresh = navigation_params.response->header_list()->get("Refresh"sv.bytes()); maybe_refresh.has_value()) {
if (auto maybe_refresh = navigation_params.response->header_list()->get("Refresh"sv); maybe_refresh.has_value()) {
// 1. Let value be the isomorphic decoding of the value of the header.
auto value = Infra::isomorphic_decode(maybe_refresh.value());

View file

@ -72,7 +72,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
Optional<u64> length {};
// 9. Let type be null.
Optional<ByteBuffer> type {};
Optional<ByteString> type {};
// 10. Switch on object.
TRY(object.visit(
@ -83,7 +83,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
length = blob->size();
// If objects type attribute is not the empty byte sequence, set type to its value.
if (!blob->type().is_empty())
type = MUST(ByteBuffer::copy(blob->type().bytes()));
type = blob->type().to_byte_string();
return {};
},
[&](ReadonlyBytes bytes) -> WebIDL::ExceptionOr<void> {
@ -103,8 +103,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
source = serialized_form_data.serialized_data;
// FIXME: Set length to unclear, see html/6424 for improving this.
// Set type to `multipart/form-data; boundary=`, followed by the multipart/form-data boundary string generated by the multipart/form-data encoding algorithm.
auto type_string = MUST(String::formatted("multipart/form-data; boundary={}", serialized_form_data.boundary));
type = MUST(ByteBuffer::copy(type_string.bytes()));
type = ByteString::formatted("multipart/form-data; boundary={}", serialized_form_data.boundary);
return {};
},
[&](GC::Root<DOMURL::URLSearchParams> const& url_search_params) -> WebIDL::ExceptionOr<void> {
@ -112,7 +111,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
auto search_params_string = url_search_params->to_string();
source = MUST(ByteBuffer::copy(search_params_string.bytes()));
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = MUST(ByteBuffer::copy("application/x-www-form-urlencoded;charset=UTF-8"sv.bytes()));
type = "application/x-www-form-urlencoded;charset=UTF-8"sv;
return {};
},
[&](String const& scalar_value_string) -> WebIDL::ExceptionOr<void> {
@ -120,7 +119,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
// Set source to the UTF-8 encoding of object.
source = MUST(ByteBuffer::copy(scalar_value_string.bytes()));
// Set type to `text/plain;charset=UTF-8`.
type = MUST(ByteBuffer::copy("text/plain;charset=UTF-8"sv.bytes()));
type = "text/plain;charset=UTF-8"sv;
return {};
},
[&](GC::Root<Streams::ReadableStream> const& stream) -> WebIDL::ExceptionOr<void> {
@ -169,7 +168,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
auto body = Infrastructure::Body::create(vm, *stream, move(source), move(length));
// 14. Return (body, type).
return Infrastructure::BodyWithType { .body = move(body), .type = move(type) };
return Infrastructure::BodyWithType { .body = body, .type = move(type) };
}
}

View file

@ -15,7 +15,7 @@ namespace Web::Fetch::Fetching {
bool cors_check(Infrastructure::Request const& request, Infrastructure::Response const& response)
{
// 1. Let origin be the result of getting `Access-Control-Allow-Origin` from responses header list.
auto origin = response.header_list()->get("Access-Control-Allow-Origin"sv.bytes());
auto origin = response.header_list()->get("Access-Control-Allow-Origin"sv);
// 2. If origin is null, then return failure.
// NOTE: Null is not `null`.
@ -23,7 +23,7 @@ bool cors_check(Infrastructure::Request const& request, Infrastructure::Response
return false;
// 3. If requests credentials mode is not "include" and origin is `*`, then return success.
if (request.credentials_mode() != Infrastructure::Request::CredentialsMode::Include && origin->span() == "*"sv.bytes())
if (request.credentials_mode() != Infrastructure::Request::CredentialsMode::Include && *origin == "*"sv)
return true;
// 4. If the result of byte-serializing a request origin with request is not origin, then return failure.
@ -35,10 +35,10 @@ bool cors_check(Infrastructure::Request const& request, Infrastructure::Response
return true;
// 6. Let credentials be the result of getting `Access-Control-Allow-Credentials` from responses header list.
auto credentials = response.header_list()->get("Access-Control-Allow-Credentials"sv.bytes());
auto credentials = response.header_list()->get("Access-Control-Allow-Credentials"sv);
// 7. If credentials is `true`, then return success.
if (credentials.has_value() && credentials->span() == "true"sv.bytes())
if (credentials == "true"sv)
return true;
// 8. Return failure.
@ -53,7 +53,7 @@ bool tao_check(Infrastructure::Request const& request, Infrastructure::Response
return false;
// 2. Let values be the result of getting, decoding, and splitting `Timing-Allow-Origin` from responses header list.
auto values = response.header_list()->get_decode_and_split("Timing-Allow-Origin"sv.bytes());
auto values = response.header_list()->get_decode_and_split("Timing-Allow-Origin"sv);
// 3. If values contains "*", then return success.
if (values.has_value() && values->contains_slow("*"sv))

View file

@ -51,6 +51,7 @@
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WorkerGlobalScope.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Loader/LoadRequest.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MixedContent/AbstractOperations.h>
@ -143,7 +144,7 @@ GC::Ref<Infrastructure::FetchController> fetch(JS::Realm& realm, Infrastructure:
// - requests client is not null, and requests clients global object is a Window object
&& request.client() && is<HTML::Window>(request.client()->global_object())
// - requests method is `GET`
&& StringView { request.method() }.equals_ignoring_ascii_case("GET"sv)
&& request.method().equals_ignoring_ascii_case("GET"sv)
// - requests unsafe-request flag is not set or requests header list is empty
&& (!request.unsafe_request() || request.header_list()->is_empty())) {
// 1. Assert: requests origin is same origin with requests clients origin.
@ -168,7 +169,7 @@ GC::Ref<Infrastructure::FetchController> fetch(JS::Realm& realm, Infrastructure:
}
// 11. If requests header list does not contain `Accept`, then:
if (!request.header_list()->contains("Accept"sv.bytes())) {
if (!request.header_list()->contains("Accept"sv)) {
// 1. Let value be `*/*`.
auto value = "*/*"sv;
@ -210,17 +211,17 @@ GC::Ref<Infrastructure::FetchController> fetch(JS::Realm& realm, Infrastructure:
}
// 4. Append (`Accept`, value) to requests header list.
auto header = Infrastructure::Header::from_string_pair("Accept"sv, value.bytes());
auto header = Infrastructure::Header::isomorphic_encode("Accept"sv, value);
request.header_list()->append(move(header));
}
// 12. If requests header list does not contain `Accept-Language`, then user agents should append
// (`Accept-Language, an appropriate header value) to requests header list.
if (!request.header_list()->contains("Accept-Language"sv.bytes())) {
if (!request.header_list()->contains("Accept-Language"sv)) {
StringBuilder accept_language;
accept_language.join(","sv, ResourceLoader::the().preferred_languages());
auto header = Infrastructure::Header::from_string_pair("Accept-Language"sv, accept_language.string_view());
auto header = Infrastructure::Header::isomorphic_encode("Accept-Language"sv, accept_language.string_view());
request.header_list()->append(move(header));
}
@ -495,13 +496,13 @@ GC::Ptr<PendingResponse> main_fetch(JS::Realm& realm, Infrastructure::FetchParam
if (request->response_tainting() == Infrastructure::Request::ResponseTainting::CORS) {
// 1. Let headerNames be the result of extracting header list values given
// `Access-Control-Expose-Headers` and responses header list.
auto header_names_or_failure = response->header_list()->extract_header_list_values("Access-Control-Expose-Headers"sv.bytes());
auto header_names_or_failure = response->header_list()->extract_header_list_values("Access-Control-Expose-Headers"sv);
if (auto* header_names = header_names_or_failure.get_pointer<Vector<ByteBuffer>>()) {
if (auto* header_names = header_names_or_failure.get_pointer<Vector<ByteString>>()) {
// 2. If requests credentials mode is not "include" and headerNames contains `*`, then set
// responses CORS-exposed header-name list to all unique header names in responses header
// list.
if (request->credentials_mode() != Infrastructure::Request::CredentialsMode::Include && header_names->contains_slow("*"sv.bytes())) {
if (request->credentials_mode() != Infrastructure::Request::CredentialsMode::Include && header_names->contains_slow("*"sv)) {
auto unique_header_names = response->header_list()->unique_names();
response->set_cors_exposed_header_name_list(move(unique_header_names));
}
@ -576,7 +577,7 @@ GC::Ptr<PendingResponse> main_fetch(JS::Realm& realm, Infrastructure::FetchParam
if (response->type() == Infrastructure::Response::Type::Opaque
&& internal_response->status() == 206
&& internal_response->range_requested()
&& !request->header_list()->contains("Range"sv.bytes())) {
&& !request->header_list()->contains("Range"sv)) {
response = internal_response = Infrastructure::Response::network_error(vm, "Response has status 206 and 'range-requested' flag set, but request has no 'Range' header"_string);
}
@ -584,7 +585,7 @@ GC::Ptr<PendingResponse> main_fetch(JS::Realm& realm, Infrastructure::FetchParam
// internalResponses status is a null body status, set internalResponses body to null and disregard
// any enqueuing toward it (if any).
// NOTE: This standardizes the error handling for servers that violate HTTP.
if (!response->is_network_error() && (StringView { request->method() }.is_one_of("HEAD"sv, "CONNECT"sv) || Infrastructure::is_null_body_status(internal_response->status())))
if (!response->is_network_error() && (request->method().is_one_of("HEAD"sv, "CONNECT"sv) || Infrastructure::is_null_body_status(internal_response->status())))
internal_response->set_body({});
// 22. If requests integrity metadata is not the empty string, then:
@ -660,7 +661,7 @@ void fetch_response_handover(JS::Realm& realm, Infrastructure::FetchParams const
// The user agent may decide to expose `Server-Timing` headers to non-secure contexts requests as well.
auto client = fetch_params.request()->client();
if (!response.is_network_error() && client != nullptr && HTML::is_secure_context(*client)) {
auto server_timing_headers = response.header_list()->get_decode_and_split("Server-Timing"sv.bytes());
auto server_timing_headers = response.header_list()->get_decode_and_split("Server-Timing"sv);
if (server_timing_headers.has_value())
timing_info->set_server_timing_headers(server_timing_headers.release_value());
}
@ -846,12 +847,10 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
// of fetching.
if (request->current_url().paths().size() == 1 && request->current_url().paths()[0] == "blank"sv) {
auto response = Infrastructure::Response::create(vm);
response->set_status_message(MUST(ByteBuffer::copy("OK"sv.bytes())));
auto header = Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html;charset=utf-8"sv);
response->header_list()->append(move(header));
response->set_status_message("OK"sv);
response->header_list()->append({ "Content-Type"sv, "text/html;charset=utf-8"sv });
response->set_body(Infrastructure::byte_sequence_as_body(realm, ""sv.bytes()));
return PendingResponse::create(vm, request, response);
}
@ -864,7 +863,7 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
auto const& blob_url_entry = request->current_url().blob_url_entry();
// 2. If requests method is not `GET` or blobURLEntry is null, then return a network error. [FILEAPI]
if (request->method() != "GET"sv.bytes() || !blob_url_entry.has_value())
if (request->method() != "GET"sv || !blob_url_entry.has_value())
return PendingResponse::create(vm, request, Infrastructure::Response::network_error(vm, "Request has an invalid 'blob:' URL"_string));
// 3. Let requestEnvironment be the result of determining the environment given request.
@ -909,21 +908,21 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
auto const& type = blob->type();
// 13. If requests header list does not contain `Range`:
if (!request->header_list()->contains("Range"sv.bytes())) {
if (!request->header_list()->contains("Range"sv)) {
// 1. Let bodyWithType be the result of safely extracting blob.
auto body_with_type = safely_extract_body(realm, blob->raw_bytes());
// 2. Set responses status message to `OK`.
response->set_status_message(MUST(ByteBuffer::copy("OK"sv.bytes())));
response->set_status_message("OK"sv);
// 3. Set responses body to bodyWithTypes body.
response->set_body(body_with_type.body);
// 4. Set responses header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ».
auto content_length_header = Infrastructure::Header::from_string_pair("Content-Length"sv, serialized_full_length);
auto content_length_header = Infrastructure::Header::isomorphic_encode("Content-Length"sv, serialized_full_length);
response->header_list()->append(move(content_length_header));
auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type);
auto content_type_header = Infrastructure::Header::isomorphic_encode("Content-Type"sv, type);
response->header_list()->append(move(content_type_header));
}
// 14. Otherwise:
@ -932,7 +931,7 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
response->set_range_requested(true);
// 2. Let rangeHeader be the result of getting `Range` from requests header list.
auto const range_header = request->header_list()->get("Range"sv.bytes()).value_or(ByteBuffer {});
auto const range_header = request->header_list()->get("Range"sv).value_or({});
// 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true.
auto maybe_range_value = Infrastructure::parse_single_range_header_value(range_header, true);
@ -984,20 +983,20 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
response->set_status(206);
// 14. Set responses status message to `Partial Content`.
response->set_status_message(MUST(ByteBuffer::copy("Partial Content"sv.bytes())));
response->set_status_message("Partial Content"sv);
// 15. Set responses header list to «
// (`Content-Length`, serializedSlicedLength),
auto content_length_header = Infrastructure::Header::from_string_pair("Content-Length"sv, serialized_sliced_length);
auto content_length_header = Infrastructure::Header::isomorphic_encode("Content-Length"sv, serialized_sliced_length);
response->header_list()->append(move(content_length_header));
// (`Content-Type`, type),
auto content_type_header = Infrastructure::Header::from_string_pair("Content-Type"sv, type);
auto content_type_header = Infrastructure::Header::isomorphic_encode("Content-Type"sv, type);
response->header_list()->append(move(content_type_header));
// (`Content-Range`, contentRange) ».
auto content_range_header = Infrastructure::Header::from_string_pair("Content-Range"sv, content_range);
auto content_range_header = Infrastructure::Header::isomorphic_encode("Content-Range"sv, content_range);
response->header_list()->append(move(content_range_header));
}
@ -1019,9 +1018,9 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
// 4. Return a new response whose status message is `OK`, header list is « (`Content-Type`, mimeType) », and
// body is dataURLStructs body as a body.
auto response = Infrastructure::Response::create(vm);
response->set_status_message(MUST(ByteBuffer::copy("OK"sv.bytes())));
response->set_status_message("OK"sv);
auto header = Infrastructure::Header::from_string_pair("Content-Type"sv, mime_type);
auto header = Infrastructure::Header::isomorphic_encode("Content-Type"sv, mime_type);
response->header_list()->append(move(header));
response->set_body(Infrastructure::byte_sequence_as_body(realm, data_url_struct.value().body));
@ -1334,13 +1333,13 @@ GC::Ptr<PendingResponse> http_redirect_fetch(JS::Realm& realm, Infrastructure::F
// 12. If one of the following is true
if (
// - internalResponses status is 301 or 302 and requests method is `POST`
((internal_response->status() == 301 || internal_response->status() == 302) && request->method() == "POST"sv.bytes())
((internal_response->status() == 301 || internal_response->status() == 302) && request->method() == "POST"sv)
// - internalResponses status is 303 and requests method is not `GET` or `HEAD`
|| (internal_response->status() == 303 && !(request->method() == "GET"sv.bytes() || request->method() == "HEAD"sv.bytes()))
|| (internal_response->status() == 303 && !(request->method() == "GET"sv || request->method() == "HEAD"sv))
// then:
) {
// 1. Set requests method to `GET` and requests body to null.
request->set_method(MUST(ByteBuffer::copy("GET"sv.bytes())));
request->set_method("GET"sv);
request->set_body({});
static constexpr Array request_body_header_names {
@ -1351,7 +1350,7 @@ GC::Ptr<PendingResponse> http_redirect_fetch(JS::Realm& realm, Infrastructure::F
};
// 2. For each headerName of request-body-header name, delete headerName from requests header list.
for (auto header_name : request_body_header_names.span())
request->header_list()->delete_(header_name.bytes());
request->header_list()->delete_(header_name);
}
// 13. If requests current URLs origin is not same origin with locationURLs origin, then for each headerName of
@ -1362,7 +1361,7 @@ GC::Ptr<PendingResponse> http_redirect_fetch(JS::Realm& realm, Infrastructure::F
"Authorization"sv
};
for (auto header_name : cors_non_wildcard_request_header_names)
request->header_list()->delete_(header_name.bytes());
request->header_list()->delete_(header_name);
}
// 14. If requests body is non-null, then set requests body to the body of the result of safely extracting
@ -1417,7 +1416,7 @@ GC::Ptr<PendingResponse> http_redirect_fetch(JS::Realm& realm, Infrastructure::F
class CachePartition : public RefCounted<CachePartition> {
public:
// https://httpwg.org/specs/rfc9111.html#constructing.responses.from.caches
GC::Ptr<Infrastructure::Response> select_response(JS::Realm& realm, URL::URL const& url, ReadonlyBytes method, Vector<Infrastructure::Header> const& headers, Vector<GC::Ptr<Infrastructure::Response>>& initial_set_of_stored_responses) const
GC::Ptr<Infrastructure::Response> select_response(JS::Realm& realm, URL::URL const& url, StringView method, Vector<Infrastructure::Header> const& headers, Vector<GC::Ptr<Infrastructure::Response>>& initial_set_of_stored_responses) const
{
// When presented with a request, a cache MUST NOT reuse a stored response unless:
@ -1461,7 +1460,7 @@ public:
store_header_and_trailer_fields(response, *cached_response->header_list());
cached_response->set_body(response.body()->clone(realm));
cached_response->set_body_info(response.body_info());
cached_response->set_method(MUST(ByteBuffer::copy(http_request.method())));
cached_response->set_method(http_request.method());
cached_response->set_status(response.status());
cached_response->url_list().append(http_request.current_url());
m_cache.set(http_request.current_url(), move(cached_response));
@ -1570,35 +1569,23 @@ private:
// https://httpwg.org/specs/rfc9111.html#update
void update_stored_header_fields(Infrastructure::Response const& response, Infrastructure::HeaderList& headers)
{
for (auto& header : *response.header_list()) {
auto name = StringView(header.name);
if (is_exempted_for_updating(name))
continue;
headers.delete_(header.name);
for (auto const& header : *response.header_list()) {
if (!is_exempted_for_updating(header.name))
headers.delete_(header.name);
}
for (auto& header : *response.header_list()) {
auto name = StringView(header.name);
if (is_exempted_for_updating(name))
continue;
headers.append(Infrastructure::Header::copy(header));
for (auto const& header : *response.header_list()) {
if (!is_exempted_for_updating(header.name))
headers.append(header);
}
}
// https://httpwg.org/specs/rfc9111.html#storing.fields
void store_header_and_trailer_fields(Infrastructure::Response const& response, Web::Fetch::Infrastructure::HeaderList& headers)
{
for (auto& header : *response.header_list()) {
auto name = StringView(header.name);
if (is_exempted_for_storage(name))
continue;
headers.append(Infrastructure::Header::copy(header));
for (auto const& header : *response.header_list()) {
if (!is_exempted_for_storage(header.name))
headers.append(header);
}
}
@ -1612,7 +1599,7 @@ private:
return false;
// - the request method is understood by the cache;
if (request.method() != "GET"sv.bytes() && request.method() != "HEAD"sv.bytes())
if (request.method() != "GET"sv && request.method() != "HEAD"sv)
return false;
// - the response status code is final (see Section 15 of [HTTP]);
@ -1787,29 +1774,22 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
: Optional<u64> {};
// 6. Let contentLengthHeaderValue be null.
auto content_length_header_value = Optional<ByteBuffer> {};
Optional<ByteString> content_length_header_value;
// 7. If httpRequests body is null and httpRequests method is `POST` or `PUT`, then set
// contentLengthHeaderValue to `0`.
if (http_request->body().has<Empty>() && StringView { http_request->method() }.is_one_of("POST"sv, "PUT"sv))
content_length_header_value = MUST(ByteBuffer::copy("0"sv.bytes()));
if (http_request->body().has<Empty>() && http_request->method().is_one_of("POST"sv, "PUT"sv))
content_length_header_value = "0"sv;
// 8. If contentLength is non-null, then set contentLengthHeaderValue to contentLength, serialized and
// isomorphic encoded.
if (content_length.has_value()) {
auto content_length_string = String::number(*content_length);
content_length_header_value = MUST(ByteBuffer::copy(content_length_string.bytes()));
}
if (content_length.has_value())
content_length_header_value = ByteString::number(*content_length);
// 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue) to
// httpRequests header list.
if (content_length_header_value.has_value()) {
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Content-Length"sv.bytes())),
.value = content_length_header_value.release_value(),
};
http_request->header_list()->append(move(header));
}
if (content_length_header_value.has_value())
http_request->header_list()->append({ "Content-Length"sv, content_length_header_value.release_value() });
// 10. If contentLength is non-null and httpRequests keepalive is true, then:
if (content_length.has_value() && http_request->keepalive()) {
@ -1849,17 +1829,12 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
}
// 11. If httpRequests referrer is a URL, then:
if (http_request->referrer().has<URL::URL>()) {
if (auto const* referrer_url = http_request->referrer().get_pointer<URL::URL>()) {
// 1. Let referrerValue be httpRequests referrer, serialized and isomorphic encoded.
auto referrer_string = http_request->referrer().get<URL::URL>().serialize();
auto referrer_value = MUST(ByteBuffer::copy(referrer_string.bytes()));
auto referrer_value = Infra::isomorphic_encode(referrer_url->serialize());
// 2. Append (`Referer`, referrerValue) to httpRequests header list.
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Referer"sv.bytes())),
.value = move(referrer_value),
};
http_request->header_list()->append(move(header));
http_request->header_list()->append({ "Referer"sv, move(referrer_value) });
}
// 12. Append a request `Origin` header for httpRequest.
@ -1873,23 +1848,18 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// 15. If httpRequests header list does not contain `User-Agent`, then user agents should append
// (`User-Agent`, default `User-Agent` value) to httpRequests header list.
if (!http_request->header_list()->contains("User-Agent"sv.bytes())) {
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("User-Agent"sv.bytes())),
.value = Infrastructure::default_user_agent_value(),
};
http_request->header_list()->append(move(header));
}
if (!http_request->header_list()->contains("User-Agent"sv))
http_request->header_list()->append({ "User-Agent"sv, Infrastructure::default_user_agent_value() });
// 16. If httpRequests cache mode is "default" and httpRequests header list contains `If-Modified-Since`,
// `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set httpRequests cache mode to
// "no-store".
if (http_request->cache_mode() == Infrastructure::Request::CacheMode::Default
&& (http_request->header_list()->contains("If-Modified-Since"sv.bytes())
|| http_request->header_list()->contains("If-None-Match"sv.bytes())
|| http_request->header_list()->contains("If-Unmodified-Since"sv.bytes())
|| http_request->header_list()->contains("If-Match"sv.bytes())
|| http_request->header_list()->contains("If-Range"sv.bytes()))) {
&& (http_request->header_list()->contains("If-Modified-Since"sv)
|| http_request->header_list()->contains("If-None-Match"sv)
|| http_request->header_list()->contains("If-Unmodified-Since"sv)
|| http_request->header_list()->contains("If-Match"sv)
|| http_request->header_list()->contains("If-Range"sv))) {
http_request->set_cache_mode(Infrastructure::Request::CacheMode::NoStore);
}
@ -1898,9 +1868,8 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// (`Cache-Control`, `max-age=0`) to httpRequests header list.
if (http_request->cache_mode() == Infrastructure::Request::CacheMode::NoCache
&& !http_request->prevent_no_cache_cache_control_header_modification()
&& !http_request->header_list()->contains("Cache-Control"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("Cache-Control"sv, "max-age=0"sv);
http_request->header_list()->append(move(header));
&& !http_request->header_list()->contains("Cache-Control"sv)) {
http_request->header_list()->append({ "Cache-Control"sv, "max-age=0"sv });
}
// 18. If httpRequests cache mode is "no-store" or "reload", then:
@ -1908,27 +1877,21 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
|| http_request->cache_mode() == Infrastructure::Request::CacheMode::Reload) {
// 1. If httpRequests header list does not contain `Pragma`, then append (`Pragma`, `no-cache`) to
// httpRequests header list.
if (!http_request->header_list()->contains("Pragma"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("Pragma"sv, "no-cache"sv);
http_request->header_list()->append(move(header));
}
if (!http_request->header_list()->contains("Pragma"sv))
http_request->header_list()->append({ "Pragma"sv, "no-cache"sv });
// 2. If httpRequests header list does not contain `Cache-Control`, then append
// (`Cache-Control`, `no-cache`) to httpRequests header list.
if (!http_request->header_list()->contains("Cache-Control"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("Cache-Control"sv, "no-cache"sv);
http_request->header_list()->append(move(header));
}
if (!http_request->header_list()->contains("Cache-Control"sv))
http_request->header_list()->append({ "Cache-Control"sv, "no-cache"sv });
}
// 19. If httpRequests header list contains `Range`, then append (`Accept-Encoding`, `identity`) to
// httpRequests header list.
// NOTE: This avoids a failure when handling content codings with a part of an encoded response.
// Additionally, many servers mistakenly ignore `Range` headers if a non-identity encoding is accepted.
if (http_request->header_list()->contains("Range"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("Accept-Encoding"sv, "identity"sv);
http_request->header_list()->append(move(header));
}
if (http_request->header_list()->contains("Range"sv))
http_request->header_list()->append({ "Accept-Encoding"sv, "identity"sv });
// 20. Modify httpRequests header list per HTTP. Do not append a given header if httpRequests header list
// contains that headers name.
@ -1940,10 +1903,8 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// more details.
//
// https://w3c.github.io/gpc/#the-sec-gpc-header-field-for-http-requests
if (ResourceLoader::the().enable_global_privacy_control() && !http_request->header_list()->contains("Sec-GPC"sv.bytes())) {
auto header = Infrastructure::Header::from_string_pair("Sec-GPC"sv, "1"sv);
http_request->header_list()->append(move(header));
}
if (ResourceLoader::the().enable_global_privacy_control() && !http_request->header_list()->contains("Sec-GPC"sv))
http_request->header_list()->append({ "Sec-GPC"sv, "1"sv });
// 21. If includeCredentials is true, then:
if (include_credentials == IncludeCredentials::Yes) {
@ -1959,13 +1920,13 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// 2. If cookies is not the empty string, then append (`Cookie`, cookies) to httpRequests header list.
if (!cookies.is_empty()) {
auto header = Infrastructure::Header::from_string_pair("Cookie"sv, cookies);
auto header = Infrastructure::Header::isomorphic_encode("Cookie"sv, cookies);
http_request->header_list()->append(move(header));
}
}
// 2. If httpRequests header list does not contain `Authorization`, then:
if (!http_request->header_list()->contains("Authorization"sv.bytes())) {
if (!http_request->header_list()->contains("Authorization"sv)) {
// 1. Let authorizationValue be null.
auto authorization_value = Optional<String> {};
@ -1987,7 +1948,7 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// 4. If authorizationValue is non-null, then append (`Authorization`, authorizationValue) to
// httpRequests header list.
if (authorization_value.has_value()) {
auto header = Infrastructure::Header::from_string_pair("Authorization"sv, *authorization_value);
auto header = Infrastructure::Header::isomorphic_encode("Authorization"sv, *authorization_value);
http_request->header_list()->append(move(header));
}
}
@ -2011,6 +1972,7 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// if any.
// NOTE: As mandated by HTTP, this still takes the `Vary` header into account.
stored_response = http_cache->select_response(realm, http_request->current_url(), http_request->method(), *http_request->header_list(), initial_set_of_stored_responses);
// 2. If storedResponse is non-null, then:
if (stored_response) {
// 1. If cache mode is "default", storedResponse is a stale-while-revalidate response,
@ -2054,13 +2016,13 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
&& http_request->cache_mode() != Infrastructure::Request::CacheMode::OnlyIfCached) {
// 1. If storedResponses header list contains `ETag`, then append (`If-None-Match`, `ETag`'s value) to httpRequests header list.
if (auto etag = stored_response->header_list()->get("ETag"sv.bytes()); etag.has_value()) {
http_request->header_list()->append(Infrastructure::Header::from_string_pair("If-None-Match"sv, *etag));
if (auto etag = stored_response->header_list()->get("ETag"sv); etag.has_value()) {
http_request->header_list()->append(Infrastructure::Header::isomorphic_encode("If-None-Match"sv, *etag));
}
// 2. If storedResponses header list contains `Last-Modified`, then append (`If-Modified-Since`, `Last-Modified`'s value) to httpRequests header list.
if (auto last_modified = stored_response->header_list()->get("Last-Modified"sv.bytes()); last_modified.has_value()) {
http_request->header_list()->append(Infrastructure::Header::from_string_pair("If-Modified-Since"sv, *last_modified));
if (auto last_modified = stored_response->header_list()->get("Last-Modified"sv); last_modified.has_value()) {
http_request->header_list()->append(Infrastructure::Header::isomorphic_encode("If-Modified-Since"sv, *last_modified));
}
}
// 3. Otherwise, set response to storedResponse and set responses cache state to "local".
@ -2100,7 +2062,7 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
auto forward_response = resolved_forward_response;
// NOTE: TRACE is omitted as it is a forbidden method in Fetch.
auto method_is_unsafe = !(StringView { http_request->method() }.is_one_of("GET"sv, "HEAD"sv, "OPTIONS"sv));
auto method_is_unsafe = !http_request->method().is_one_of("GET"sv, "HEAD"sv, "OPTIONS"sv);
// 3. If httpRequests method is unsafe and forwardResponses status is in the range 200 to 399, inclusive,
// invalidate appropriate stored responses in httpCache, as per the "Invalidation" chapter of HTTP
@ -2145,7 +2107,7 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
response->set_url_list(http_request->url_list());
// 12. If httpRequests header list contains `Range`, then set responses range-requested flag.
if (http_request->header_list()->contains("Range"sv.bytes()))
if (http_request->header_list()->contains("Range"sv))
response->set_range_requested(true);
// 13. Set responses request-includes-credentials to includeCredentials.
@ -2161,7 +2123,7 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
&& request->traversable_for_user_prompts().has<GC::Ptr<HTML::TraversableNavigable>>()
// AD-HOC: Require at least one WWW-Authenticate header to be set before automatically retrying an authenticated
// request (see rule 1 below). See: https://github.com/whatwg/fetch/issues/1766
&& request->header_list()->contains("WWW-Authenticate"sv.bytes())) {
&& request->header_list()->contains("WWW-Authenticate"sv)) {
// 1. Needs testing: multiple `WWW-Authenticate` headers, missing, parsing issues.
// (Red box in the spec, no-op)
@ -2324,11 +2286,11 @@ GC::Ref<PendingResponse> nonstandard_resource_loader_file_or_http_network_fetch(
LoadRequest load_request;
load_request.set_url(request->current_url());
load_request.set_page(page);
load_request.set_method(ByteString::copy(request->method()));
load_request.set_method(request->method());
load_request.set_store_set_cookie_headers(include_credentials == IncludeCredentials::Yes);
for (auto const& header : *request->header_list())
load_request.set_header(ByteString::copy(header.name), ByteString::copy(header.value));
load_request.set_header(header.name, header.value);
if (auto const* body = request->body().get_pointer<GC::Ref<Infrastructure::Body>>()) {
(*body)->source().visit(
@ -2390,7 +2352,7 @@ GC::Ref<PendingResponse> nonstandard_resource_loader_file_or_http_network_fetch(
response->set_status(status_code.value_or(200));
if (reason_phrase.has_value())
response->set_status_message(MUST(ByteBuffer::copy(reason_phrase.value().bytes())));
response->set_status_message(reason_phrase->to_byte_string());
(void)request;
if constexpr (WEB_FETCH_DEBUG) {
@ -2401,10 +2363,8 @@ GC::Ref<PendingResponse> nonstandard_resource_loader_file_or_http_network_fetch(
log_response(status_code, response_headers, ReadonlyBytes {});
}
for (auto const& [name, value] : response_headers.headers()) {
auto header = Infrastructure::Header::from_latin1_pair(name, value);
response->header_list()->append(move(header));
}
for (auto const& [name, value] : response_headers.headers())
response->header_list()->append({ name, value });
// 14. Set responses body to a new body whose stream is stream.
response->set_body(Infrastructure::Body::create(vm, stream));
@ -2454,7 +2414,7 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
// requests initiator, destination is requests destination, origin is requests origin, referrer is requests referrer,
// referrer policy is requests referrer policy, mode is "cors", and response tainting is "cors".
auto preflight = Fetch::Infrastructure::Request::create(vm);
preflight->set_method(MUST(ByteBuffer::copy("OPTIONS"sv.bytes())));
preflight->set_method("OPTIONS"sv);
preflight->set_url_list(request.url_list());
preflight->set_initiator(request.initiator());
preflight->set_destination(request.destination());
@ -2465,11 +2425,10 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
preflight->set_response_tainting(Infrastructure::Request::ResponseTainting::CORS);
// 2. Append (`Accept`, `*/*`) to preflights header list.
auto temp_header = Infrastructure::Header::from_string_pair("Accept"sv, "*/*"sv);
preflight->header_list()->append(move(temp_header));
preflight->header_list()->append({ "Accept"sv, "*/*"sv });
// 3. Append (`Access-Control-Request-Method`, requests method) to preflights header list.
temp_header = Infrastructure::Header::from_string_pair("Access-Control-Request-Method"sv, request.method());
auto temp_header = Infrastructure::Header::isomorphic_encode("Access-Control-Request-Method"sv, request.method());
preflight->header_list()->append(move(temp_header));
// 4. Let headers be the CORS-unsafe request-header names with requests header list.
@ -2480,22 +2439,10 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
// 1. Let value be the items in headers separated from each other by `,`.
// NOTE: This intentionally does not use combine, as 0x20 following 0x2C is not the way this was implemented,
// for better or worse.
ByteBuffer value;
bool first = true;
for (auto const& header : headers) {
if (!first)
value.append(',');
value.append(header);
first = false;
}
auto value = ByteString::join(',', headers);
// 2. Append (`Access-Control-Request-Headers`, value) to preflights header list.
temp_header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Access-Control-Request-Headers"sv.bytes())),
.value = move(value),
};
preflight->header_list()->append(move(temp_header));
preflight->header_list()->append({ "Access-Control-Request-Headers"sv, move(value) });
}
// 6. Let response be the result of running HTTP-network-or-cache fetch given a new fetch params whose request is preflight.
@ -2514,18 +2461,17 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
// NOTE: The CORS check is done on request rather than preflight to ensure the correct credentials mode is used.
if (cors_check(request, response) && Infrastructure::is_ok_status(response->status())) {
// 1. Let methods be the result of extracting header list values given `Access-Control-Allow-Methods` and responses header list.
auto methods_or_failure = response->header_list()->extract_header_list_values("Access-Control-Allow-Methods"sv.bytes());
auto methods_or_failure = response->header_list()->extract_header_list_values("Access-Control-Allow-Methods"sv);
// 2. Let headerNames be the result of extracting header list values given `Access-Control-Allow-Headers` and
// responses header list.
auto header_names_or_failure = response->header_list()->extract_header_list_values("Access-Control-Allow-Headers"sv.bytes());
auto header_names_or_failure = response->header_list()->extract_header_list_values("Access-Control-Allow-Headers"sv);
// 3. If either methods or headerNames is failure, return a network error.
if (methods_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>()) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Methods in the CORS-preflight response is syntactically invalid"_string));
return;
}
if (header_names_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>()) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Headers in the CORS-preflight response is syntactically invalid"_string));
return;
@ -2533,29 +2479,29 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
// NOTE: We treat "methods_or_failure" being `Empty` as empty Vector here.
auto methods = methods_or_failure.visit(
[](Vector<ByteBuffer>& methods) { return move(methods); },
[](auto) -> Vector<ByteBuffer> { return {}; });
[](Vector<ByteString>& methods) { return move(methods); },
[](auto) -> Vector<ByteString> { return {}; });
// NOTE: We treat "header_names_or_failure" being `Empty` as empty Vector here.
auto header_names = header_names_or_failure.visit(
[](Vector<ByteBuffer>& header_names) { return move(header_names); },
[](auto) -> Vector<ByteBuffer> { return {}; });
[](Vector<ByteString>& header_names) { return move(header_names); },
[](auto) -> Vector<ByteString> { return {}; });
// 4. If methods is null and requests use-CORS-preflight flag is set, then set methods to a new list containing requests method.
// NOTE: This ensures that a CORS-preflight fetch that happened due to requests use-CORS-preflight flag being set is cached.
if (methods.is_empty() && request.use_cors_preflight())
methods = Vector { TRY_OR_IGNORE(ByteBuffer::copy(request.method())) };
methods = { request.method() };
// 5. If requests method is not in methods, requests method is not a CORS-safelisted method, and requests credentials mode
// is "include" or methods does not contain `*`, then return a network error.
if (!methods.contains_slow(request.method()) && !Infrastructure::is_cors_safelisted_method(request.method())) {
if (request.credentials_mode() == Infrastructure::Request::CredentialsMode::Include) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("Non-CORS-safelisted method '{}' not found in the CORS-preflight response's Access-Control-Allow-Methods header (the header may be missing). '*' is not allowed as the main request includes credentials.", StringView { request.method() }))));
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("Non-CORS-safelisted method '{}' not found in the CORS-preflight response's Access-Control-Allow-Methods header (the header may be missing). '*' is not allowed as the main request includes credentials.", request.method()))));
return;
}
if (!methods.contains_slow("*"sv.bytes())) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("Non-CORS-safelisted method '{}' not found in the CORS-preflight response's Access-Control-Allow-Methods header and there was no '*' entry. The header may be missing.", StringView { request.method() }))));
if (!methods.contains_slow("*"sv)) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("Non-CORS-safelisted method '{}' not found in the CORS-preflight response's Access-Control-Allow-Methods header and there was no '*' entry. The header may be missing.", request.method()))));
return;
}
}
@ -2567,14 +2513,14 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
bool is_in_header_names = false;
for (auto const& allowed_header_name : header_names) {
if (StringView { allowed_header_name }.equals_ignoring_ascii_case(header.name)) {
if (allowed_header_name.equals_ignoring_ascii_case(header.name)) {
is_in_header_names = true;
break;
}
}
if (!is_in_header_names) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("Main request contains the header '{}' that is not specified in the CORS-preflight response's Access-Control-Allow-Headers header (the header may be missing). '*' does not capture this header.", StringView { header.name }))));
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("Main request contains the header '{}' that is not specified in the CORS-preflight response's Access-Control-Allow-Headers header (the header may be missing). '*' does not capture this header.", header.name))));
return;
}
}
@ -2588,7 +2534,7 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
bool is_in_header_names = false;
for (auto const& header_name : header_names) {
if (StringView { unsafe_name }.equals_ignoring_ascii_case(header_name)) {
if (unsafe_name.equals_ignoring_ascii_case(header_name)) {
is_in_header_names = true;
break;
}
@ -2596,12 +2542,12 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
if (!is_in_header_names) {
if (request.credentials_mode() == Infrastructure::Request::CredentialsMode::Include) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("CORS-unsafe request-header '{}' not found in the CORS-preflight response's Access-Control-Allow-Headers header (the header may be missing). '*' is not allowed as the main request includes credentials.", StringView { unsafe_name }))));
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("CORS-unsafe request-header '{}' not found in the CORS-preflight response's Access-Control-Allow-Headers header (the header may be missing). '*' is not allowed as the main request includes credentials.", unsafe_name))));
return;
}
if (!header_names.contains_slow("*"sv.bytes())) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("CORS-unsafe request-header '{}' not found in the CORS-preflight response's Access-Control-Allow-Headers header and there was no '*' entry. The header may be missing.", StringView { unsafe_name }))));
if (!header_names.contains_slow("*"sv)) {
returned_pending_response->resolve(Infrastructure::Response::network_error(vm, TRY_OR_IGNORE(String::formatted("CORS-unsafe request-header '{}' not found in the CORS-preflight response's Access-Control-Allow-Headers header and there was no '*' entry. The header may be missing.", unsafe_name))));
return;
}
}
@ -2644,19 +2590,12 @@ void set_sec_fetch_dest_header(Infrastructure::Request& request)
// FIXME: This is handled below, as Serenity doesn't have APIs for RFC 8941.
// 3. If rs destination is the empty string, set headers value to the string "empty". Otherwise, set headers value to rs destination.
ByteBuffer header_value;
if (!request.destination().has_value()) {
header_value = MUST(ByteBuffer::copy("empty"sv.bytes()));
} else {
header_value = MUST(ByteBuffer::copy(Infrastructure::request_destination_to_string(request.destination().value()).bytes()));
}
auto value = request.destination().has_value()
? Infrastructure::request_destination_to_string(*request.destination())
: "empty"sv;
// 4. Set a structured field value `Sec-Fetch-Dest`/header in rs header list.
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Sec-Fetch-Dest"sv.bytes())),
.value = move(header_value),
};
request.header_list()->append(move(header));
request.header_list()->append({ "Sec-Fetch-Dest"sv, value });
}
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest
@ -2669,14 +2608,10 @@ void set_sec_fetch_mode_header(Infrastructure::Request& request)
// FIXME: This is handled below, as Serenity doesn't have APIs for RFC 8941.
// 3. Set headers value to rs mode.
auto header_value = MUST(ByteBuffer::copy(Infrastructure::request_mode_to_string(request.mode()).bytes()));
auto value = Infrastructure::request_mode_to_string(request.mode());
// 4. Set a structured field value `Sec-Fetch-Mode`/header in rs header list.
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Sec-Fetch-Mode"sv.bytes())),
.value = move(header_value),
};
request.header_list()->append(move(header));
request.header_list()->append({ "Sec-Fetch-Mode"sv, value });
}
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-site
@ -2689,39 +2624,35 @@ void set_sec_fetch_site_header(Infrastructure::Request& request)
// FIXME: This is handled below, as Serenity doesn't have APIs for RFC 8941.
// 3. Set headers value to same-origin.
auto header_value = "same-origin"sv;
auto value = "same-origin"sv;
// FIXME: 4. If r is a navigation request that was explicitly caused by a users interaction with the user agent (by typing an address
// into the user agent directly, for example, or by clicking a bookmark, etc.), then set headers value to none.
// 5. If headers value is not none, then for each url in rs url list:
if (!header_value.equals_ignoring_ascii_case("none"sv)) {
if (!value.equals_ignoring_ascii_case("none"sv)) {
VERIFY(request.origin().has<URL::Origin>());
auto& request_origin = request.origin().get<URL::Origin>();
auto const& request_origin = request.origin().get<URL::Origin>();
for (auto& url : request.url_list()) {
for (auto const& url : request.url_list()) {
// 1. If url is same origin with rs origin, continue.
if (url.origin().is_same_origin(request_origin))
continue;
// 2. Set headers value to cross-site.
header_value = "cross-site"sv;
value = "cross-site"sv;
// 3. If rs origin is not same site with urls origin, then break.
if (!request_origin.is_same_site(url.origin()))
break;
// 4. Set headers value to same-site.
header_value = "same-site"sv;
value = "same-site"sv;
}
}
// 6. Set a structured field value `Sec-Fetch-Site`/header in rs header list.
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Sec-Fetch-Site"sv.bytes())),
.value = MUST(ByteBuffer::copy(header_value.bytes())),
};
request.header_list()->append(move(header));
request.header_list()->append({ "Sec-Fetch-Site"sv, value });
}
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-user
@ -2739,14 +2670,10 @@ void set_sec_fetch_user_header(Infrastructure::Request& request)
// 4. Set headers value to true.
// NOTE: See https://datatracker.ietf.org/doc/html/rfc8941#name-booleans for boolean format in RFC 8941.
auto header_value = MUST(ByteBuffer::copy("?1"sv.bytes()));
static ByteString value = "?1"sv;
// 5. Set a structured field value `Sec-Fetch-User`/header in rs header list.
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Sec-Fetch-User"sv.bytes())),
.value = move(header_value),
};
request.header_list()->append(move(header));
request.header_list()->append({ "Sec-Fetch-User"sv, value });
}
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-append-the-fetch-metadata-headers-for-a-request

View file

@ -57,20 +57,19 @@ void Headers::visit_edges(JS::Cell::Visitor& visitor)
WebIDL::ExceptionOr<void> Headers::append(String const& name_string, String const& value_string)
{
// The append(name, value) method steps are to append (name, value) to this.
auto header = Infrastructure::Header::from_string_pair(name_string, value_string);
auto header = Infrastructure::Header::isomorphic_encode(name_string, value_string);
TRY(append(move(header)));
return {};
}
// https://fetch.spec.whatwg.org/#dom-headers-delete
WebIDL::ExceptionOr<void> Headers::delete_(String const& name_string)
WebIDL::ExceptionOr<void> Headers::delete_(String const& name)
{
// The delete(name) method steps are:
auto name = name_string.bytes();
// 1. If validating (name, ``) for headers returns false, then return.
// NOTE: Passing a dummy header value ought not to have any negative repercussions.
auto header = Infrastructure::Header::from_string_pair(name, ""sv);
auto header = Infrastructure::Header::isomorphic_encode(name, ""sv);
if (!TRY(validate(header)))
return {};
@ -93,10 +92,9 @@ WebIDL::ExceptionOr<void> Headers::delete_(String const& name_string)
}
// https://fetch.spec.whatwg.org/#dom-headers-get
WebIDL::ExceptionOr<Optional<String>> Headers::get(String const& name_string)
WebIDL::ExceptionOr<Optional<String>> Headers::get(String const& name)
{
// The get(name) method steps are:
auto name = name_string.bytes();
// 1. If name is not a header name, then throw a TypeError.
if (!Infrastructure::is_header_name(name))
@ -114,23 +112,22 @@ Vector<String> Headers::get_set_cookie()
auto values = Vector<String> {};
// 1. If thiss header list does not contain `Set-Cookie`, then return « ».
if (!m_header_list->contains("Set-Cookie"sv.bytes()))
if (!m_header_list->contains("Set-Cookie"sv))
return values;
// 2. Return the values of all headers in thiss header list whose name is a byte-case-insensitive match for
// `Set-Cookie`, in order.
for (auto const& header : *m_header_list) {
if (StringView { header.name }.equals_ignoring_ascii_case("Set-Cookie"sv))
if (header.name.equals_ignoring_ascii_case("Set-Cookie"sv))
values.append(Infra::isomorphic_decode(header.value));
}
return values;
}
// https://fetch.spec.whatwg.org/#dom-headers-has
WebIDL::ExceptionOr<bool> Headers::has(String const& name_string)
WebIDL::ExceptionOr<bool> Headers::has(String const& name)
{
// The has(name) method steps are:
auto name = name_string.bytes();
// 1. If name is not a header name, then throw a TypeError.
if (!Infrastructure::is_header_name(name))
@ -141,16 +138,14 @@ WebIDL::ExceptionOr<bool> Headers::has(String const& name_string)
}
// https://fetch.spec.whatwg.org/#dom-headers-set
WebIDL::ExceptionOr<void> Headers::set(String const& name_string, String const& value_string)
WebIDL::ExceptionOr<void> Headers::set(String const& name, String const& value)
{
// The set(name, value) method steps are:
auto name = name_string.bytes();
auto value = value_string.bytes();
// 1. Normalize value.
auto normalized_value = Infrastructure::normalize_header_value(value);
auto header = Infrastructure::Header::from_string_pair(name, normalized_value);
auto header = Infrastructure::Header::isomorphic_encode(name, normalized_value);
// 2. If validating (name, value) for headers returns false, then return.
if (!TRY(validate(header)))
@ -252,17 +247,15 @@ WebIDL::ExceptionOr<void> Headers::append(Infrastructure::Header header)
// 2. If temporaryValue is null, then set temporaryValue to value.
if (!temporary_value.has_value()) {
temporary_value = MUST(ByteBuffer::copy(value));
temporary_value = move(value);
}
// 3. Otherwise, set temporaryValue to temporaryValue, followed by 0x2C 0x20, followed by value.
else {
temporary_value->append(0x2c);
temporary_value->append(0x20);
temporary_value->append(value);
temporary_value = ByteString::formatted("{}, {}", *temporary_value, value);
}
auto temporary_header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy(name)),
.name = move(name),
.value = temporary_value.release_value(),
};
@ -294,7 +287,7 @@ WebIDL::ExceptionOr<void> Headers::fill(HeadersInit const& object)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Array must contain header key/value pair"sv };
// 2. Append (header[0], header[1]) to headers.
auto header = Infrastructure::Header::from_string_pair(entry[0], entry[1]);
auto header = Infrastructure::Header::isomorphic_encode(entry[0], entry[1]);
TRY(append(move(header)));
}
return {};
@ -302,7 +295,7 @@ WebIDL::ExceptionOr<void> Headers::fill(HeadersInit const& object)
// 2. Otherwise, object is a record, then for each key → value of object, append (key, value) to headers.
[&](OrderedHashMap<String, String> const& object) -> WebIDL::ExceptionOr<void> {
for (auto const& entry : object) {
auto header = Infrastructure::Header::from_string_pair(entry.key, entry.value);
auto header = Infrastructure::Header::isomorphic_encode(entry.key, entry.value);
TRY(append(move(header)));
}
return {};
@ -321,7 +314,7 @@ void Headers::remove_privileged_no_cors_request_headers()
// 1. For each headerName of privileged no-CORS request-header names:
for (auto const& header_name : privileged_no_cors_request_header_names) {
// 1. Delete headerName from headerss header list.
m_header_list->delete_(header_name.bytes());
m_header_list->delete_(header_name);
}
}

View file

@ -74,7 +74,7 @@ private:
// A body with type is a tuple that consists of a body (a body) and a type (a header value or null).
struct BodyWithType {
GC::Ref<Body> body;
Optional<ByteBuffer> type;
Optional<ByteString> type;
};
WEB_API GC::Ref<Body> byte_sequence_as_body(JS::Realm&, ReadonlyBytes);

View file

@ -10,16 +10,13 @@
#include <AK/Checked.h>
#include <AK/GenericLexer.h>
#include <AK/QuickSort.h>
#include <AK/ScopeGuard.h>
#include <AK/StringUtils.h>
#include <LibGC/Heap.h>
#include <LibJS/Runtime/VM.h>
#include <LibRegex/Regex.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Fetch/Infrastructure/HTTP.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h>
#include <LibWeb/Infra/ByteSequences.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MimeSniff/MimeType.h>
@ -28,71 +25,38 @@ namespace Web::Fetch::Infrastructure {
GC_DEFINE_ALLOCATOR(HeaderList);
template<typename T>
requires(IsSameIgnoringCV<T, u8>) struct CaseInsensitiveBytesTraits : public Traits<Span<T>> {
static constexpr bool equals(Span<T> const& a, Span<T> const& b)
{
return StringView { a }.equals_ignoring_ascii_case(StringView { b });
}
static constexpr unsigned hash(Span<T> const& span)
{
if (span.is_empty())
return 0;
return AK::case_insensitive_string_hash(reinterpret_cast<char const*>(span.data()), span.size());
}
};
Header Header::copy(Header const& header)
Header Header::isomorphic_encode(StringView name, StringView value)
{
return Header {
.name = MUST(ByteBuffer::copy(header.name)),
.value = MUST(ByteBuffer::copy(header.value)),
};
}
Header Header::from_string_pair(StringView name, StringView value)
{
return Header {
return {
.name = Infra::isomorphic_encode(name),
.value = Infra::isomorphic_encode(value),
};
}
Header Header::from_latin1_pair(StringView name, StringView value)
{
return Header {
.name = MUST(ByteBuffer::copy(name.bytes())),
.value = MUST(ByteBuffer::copy(value.bytes())),
};
}
// https://fetch.spec.whatwg.org/#extract-header-values
Optional<Vector<ByteBuffer>> Header::extract_header_values() const
Optional<Vector<ByteString>> Header::extract_header_values() const
{
// FIXME: 1. If parsing headers value, per the ABNF for headers name, fails, then return failure.
// FIXME: 2. Return one or more values resulting from parsing headers value, per the ABNF for headers name.
// For now we only parse some headers that are of the ABNF list form "#something"
if (StringView { name }.is_one_of_ignoring_ascii_case(
if (name.is_one_of_ignoring_ascii_case(
"Access-Control-Request-Headers"sv,
"Access-Control-Expose-Headers"sv,
"Access-Control-Allow-Headers"sv,
"Access-Control-Allow-Methods"sv)
&& !value.is_empty()) {
auto split_values = StringView { value }.split_view(',');
Vector<ByteBuffer> trimmed_values;
Vector<ByteString> trimmed_values;
for (auto const& value : split_values) {
auto trimmed_value = value.trim(" \t"sv);
auto trimmed_value_as_byte_buffer = MUST(ByteBuffer::copy(trimmed_value.bytes()));
trimmed_values.append(move(trimmed_value_as_byte_buffer));
}
value.view().for_each_split_view(',', SplitBehavior::Nothing, [&](auto value) {
trimmed_values.append(value.trim(" \t"sv));
});
return trimmed_values;
}
// This always ignores the ABNF rules for now and returns the header value as a single list item.
return Vector { MUST(ByteBuffer::copy(value)) };
return Vector { value };
}
GC::Ref<HeaderList> HeaderList::create(JS::VM& vm)
@ -101,16 +65,17 @@ GC::Ref<HeaderList> HeaderList::create(JS::VM& vm)
}
// https://fetch.spec.whatwg.org/#header-list-contains
bool HeaderList::contains(ReadonlyBytes name) const
bool HeaderList::contains(StringView name) const
{
// A header list list contains a header name name if list contains a header whose name is a byte-case-insensitive match for name.
// A header list list contains a header name name if list contains a header whose name is a byte-case-insensitive
// match for name.
return any_of(*this, [&](auto const& header) {
return StringView { header.name }.equals_ignoring_ascii_case(name);
return header.name.equals_ignoring_ascii_case(name);
});
}
// https://fetch.spec.whatwg.org/#concept-header-list-get
Optional<ByteBuffer> HeaderList::get(ReadonlyBytes name) const
Optional<ByteString> HeaderList::get(StringView name) const
{
// To get a header name name from a header list list, run these steps:
@ -118,25 +83,24 @@ Optional<ByteBuffer> HeaderList::get(ReadonlyBytes name) const
if (!contains(name))
return {};
// 2. Return the values of all headers in list whose name is a byte-case-insensitive match for name, separated from each other by 0x2C 0x20, in order.
ByteBuffer buffer;
auto first = true;
// 2. Return the values of all headers in list whose name is a byte-case-insensitive match for name, separated from
// each other by 0x2C 0x20, in order.
StringBuilder builder;
for (auto const& header : *this) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name))
if (!header.name.equals_ignoring_ascii_case(name))
continue;
if (first) {
first = false;
} else {
buffer.append(0x2c);
buffer.append(0x20);
}
buffer.append(header.value);
if (!builder.is_empty())
builder.append(", "sv);
builder.append(header.value);
}
return buffer;
return builder.to_byte_string();
}
// https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
Optional<Vector<String>> HeaderList::get_decode_and_split(ReadonlyBytes name) const
Optional<Vector<String>> HeaderList::get_decode_and_split(StringView name) const
{
// To get, decode, and split a header name name from header list list, run these steps:
@ -155,28 +119,28 @@ Optional<Vector<String>> HeaderList::get_decode_and_split(ReadonlyBytes name) co
void HeaderList::append(Header header)
{
// To append a header (name, value) to a header list list, run these steps:
// NOTE: Can't use structured bindings captured in the lambda due to https://github.com/llvm/llvm-project/issues/48582
auto& name = header.name;
// 1. If list contains name, then set name to the first such headers name.
// NOTE: This reuses the casing of the name of the header already in list, if any. If there are multiple matched headers their names will all be identical.
if (contains(name)) {
auto matching_header = first_matching([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
});
name.overwrite(0, matching_header->name.data(), matching_header->name.size());
}
// NOTE: This reuses the casing of the name of the header already in list, if any. If there are multiple matched
// headers their names will all be identical.
auto matching_header = first_matching([&](auto const& existing_header) {
return existing_header.name.equals_ignoring_ascii_case(header.name);
});
if (matching_header.has_value())
header.name = matching_header->name;
// 2. Append (name, value) to list.
Vector<Header>::append(move(header));
Vector::append(move(header));
}
// https://fetch.spec.whatwg.org/#concept-header-list-delete
void HeaderList::delete_(ReadonlyBytes name)
void HeaderList::delete_(StringView name)
{
// To delete a header name name from a header list list, remove all headers whose name is a byte-case-insensitive match for name from list.
// To delete a header name name from a header list list, remove all headers whose name is a byte-case-insensitive
// match for name from list.
remove_all_matching([&](auto const& header) {
return StringView { header.name }.equals_ignoring_ascii_case(name);
return header.name.equals_ignoring_ascii_case(name);
});
}
@ -184,23 +148,21 @@ void HeaderList::delete_(ReadonlyBytes name)
void HeaderList::set(Header header)
{
// To set a header (name, value) in a header list list, run these steps:
// NOTE: Can't use structured bindings captured in the lambda due to https://github.com/llvm/llvm-project/issues/48582
auto const& name = header.name;
auto const& value = header.value;
// 1. If list contains name, then set the value of the first such header to value and remove the others.
if (contains(name)) {
auto matching_index = find_if([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
}).index();
auto& matching_header = at(matching_index);
matching_header.value = MUST(ByteBuffer::copy(value));
auto it = find_if([&](auto const& existing_header) {
return existing_header.name.equals_ignoring_ascii_case(header.name);
});
if (it != end()) {
it->value = move(header.value);
size_t i = 0;
remove_all_matching([&](auto const& existing_header) {
ScopeGuard increment_i = [&]() { i++; };
if (i <= matching_index)
if (i++ <= it.index())
return false;
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
return existing_header.name.equals_ignoring_ascii_case(it->name);
});
}
// 2. Otherwise, append header (name, value) to list.
@ -213,18 +175,15 @@ void HeaderList::set(Header header)
void HeaderList::combine(Header header)
{
// To combine a header (name, value) in a header list list, run these steps:
// NOTE: Can't use structured bindings captured in the lambda due to https://github.com/llvm/llvm-project/issues/48582
auto const& name = header.name;
auto const& value = header.value;
// 1. If list contains name, then set the value of the first such header to its value, followed by 0x2C 0x20, followed by value.
if (contains(name)) {
auto matching_header = first_matching([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name);
});
matching_header->value.append(0x2c);
matching_header->value.append(0x20);
matching_header->value.append(value);
// 1. If list contains name, then set the value of the first such header to its value, followed by 0x2C 0x20,
// followed by value.
auto matching_header = first_matching([&](auto const& existing_header) {
return existing_header.name.equals_ignoring_ascii_case(header.name);
});
if (matching_header.has_value()) {
matching_header->value = ByteString::formatted("{}, {}", matching_header->value, header.value);
}
// 2. Otherwise, append (name, value) to list.
else {
@ -240,24 +199,27 @@ Vector<Header> HeaderList::sort_and_combine() const
// 1. Let headers be an empty list of headers with the key being the name and value the value.
Vector<Header> headers;
// 2. Let names be the result of convert header names to a sorted-lowercase set with all the names of the headers in list.
Vector<ReadonlyBytes> names_list;
// 2. Let names be the result of convert header names to a sorted-lowercase set with all the names of the headers
// in list.
Vector<ByteString> names_list;
names_list.ensure_capacity(size());
for (auto const& header : *this)
names_list.unchecked_append(header.name);
auto names = convert_header_names_to_a_sorted_lowercase_set(names_list);
// 3. For each name of names:
for (auto& name : names) {
// 1. If name is `set-cookie`, then:
if (name == "set-cookie"sv.bytes()) {
// 1. Let values be a list of all values of headers in list whose name is a byte-case-insensitive match for name, in order.
if (name == "set-cookie"sv) {
// 1. Let values be a list of all values of headers in list whose name is a byte-case-insensitive match for
// name, in order.
// 2. For each value of values:
for (auto const& [header_name, value] : *this) {
if (StringView { header_name }.equals_ignoring_ascii_case(name)) {
if (header_name.equals_ignoring_ascii_case(name)) {
// 1. Append (name, value) to headers.
auto header = Header::from_string_pair(name, value);
headers.append(move(header));
headers.append({ name, value });
}
}
}
@ -270,11 +232,7 @@ Vector<Header> HeaderList::sort_and_combine() const
VERIFY(value.has_value());
// 3. Append (name, value) to headers.
auto header = Header {
.name = move(name),
.value = value.release_value(),
};
headers.append(move(header));
headers.empend(move(name), value.release_value());
}
}
@ -283,7 +241,7 @@ Vector<Header> HeaderList::sort_and_combine() const
}
// https://fetch.spec.whatwg.org/#extract-header-list-values
Variant<Empty, Vector<ByteBuffer>, HeaderList::ExtractHeaderParseFailure> HeaderList::extract_header_list_values(ReadonlyBytes name) const
Variant<Empty, Vector<ByteString>, HeaderList::ExtractHeaderParseFailure> HeaderList::extract_header_list_values(StringView name) const
{
// 1. If list does not contain name, then return null.
if (!contains(name))
@ -293,11 +251,11 @@ Variant<Empty, Vector<ByteBuffer>, HeaderList::ExtractHeaderParseFailure> Header
// NOTE: If different error handling is needed, extract the desired header first.
// 3. Let values be an empty list.
auto values = Vector<ByteBuffer> {};
Vector<ByteString> values;
// 4. For each header header list contains whose name is name:
for (auto const& header : *this) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name))
if (!header.name.equals_ignoring_ascii_case(name))
continue;
// 1. Let extract be the result of extracting header values from header.
@ -319,7 +277,7 @@ Variant<Empty, Vector<ByteBuffer>, HeaderList::ExtractHeaderParseFailure> Header
Variant<Empty, u64, HeaderList::ExtractLengthFailure> HeaderList::extract_length() const
{
// 1. Let values be the result of getting, decoding, and splitting `Content-Length` from headers.
auto values = get_decode_and_split("Content-Length"sv.bytes());
auto values = get_decode_and_split("Content-Length"sv);
// 2. If values is null, then return null.
if (!values.has_value())
@ -363,7 +321,7 @@ Optional<MimeSniff::MimeType> HeaderList::extract_mime_type() const
Optional<MimeSniff::MimeType> mime_type;
// 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers.
auto values = get_decode_and_split("Content-Type"sv.bytes());
auto values = get_decode_and_split("Content-Type"sv);
// 5. If values is null, then return failure.
if (!values.has_value())
@ -406,61 +364,65 @@ Optional<MimeSniff::MimeType> HeaderList::extract_mime_type() const
}
// Non-standard
Vector<ByteBuffer> HeaderList::unique_names() const
Vector<ByteString> HeaderList::unique_names() const
{
Vector<ByteBuffer> header_names_set;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen;
Vector<ByteString> header_names_set;
HashTable<StringView, CaseInsensitiveStringTraits> header_names_seen;
for (auto const& header : *this) {
if (header_names_seen.contains(header.name))
continue;
header_names_set.append(header.name);
header_names_seen.set(header.name);
header_names_set.append(MUST(ByteBuffer::copy(header.name)));
}
return header_names_set;
}
// https://fetch.spec.whatwg.org/#header-name
bool is_header_name(ReadonlyBytes header_name)
bool is_header_name(StringView header_name)
{
// A header name is a byte sequence that matches the field-name token production.
Regex<ECMA262Parser> regex { R"~~~(^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$)~~~" };
return regex.has_match(StringView { header_name });
return regex.has_match(header_name);
}
// https://fetch.spec.whatwg.org/#header-value
bool is_header_value(ReadonlyBytes header_value)
bool is_header_value(StringView header_value)
{
// A header value is a byte sequence that matches the following conditions:
// - Has no leading or trailing HTTP tab or space bytes.
// - Contains no 0x00 (NUL) or HTTP newline bytes.
if (header_value.is_empty())
return true;
auto first_byte = header_value[0];
auto last_byte = header_value[header_value.size() - 1];
if (HTTP_TAB_OR_SPACE_BYTES.span().contains_slow(first_byte) || HTTP_TAB_OR_SPACE_BYTES.span().contains_slow(last_byte))
auto last_byte = header_value[header_value.length() - 1];
if (is_http_tab_or_space(first_byte) || is_http_tab_or_space(last_byte))
return false;
return !any_of(header_value, [](auto byte) {
return byte == 0x00 || HTTP_NEWLINE_BYTES.span().contains_slow(byte);
return byte == 0x00 || is_http_newline(byte);
});
}
// https://fetch.spec.whatwg.org/#concept-header-value-normalize
ByteBuffer normalize_header_value(ReadonlyBytes potential_value)
ByteString normalize_header_value(StringView potential_value)
{
// To normalize a byte sequence potentialValue, remove any leading and trailing HTTP whitespace bytes from potentialValue.
// To normalize a byte sequence potentialValue, remove any leading and trailing HTTP whitespace bytes from
// potentialValue.
if (potential_value.is_empty())
return {};
auto trimmed = StringView { potential_value }.trim(HTTP_WHITESPACE, TrimMode::Both);
return MUST(ByteBuffer::copy(trimmed.bytes()));
return potential_value.trim(HTTP_WHITESPACE, TrimMode::Both);
}
// https://fetch.spec.whatwg.org/#forbidden-header-name
bool is_forbidden_request_header(Header const& header)
{
// A header (name, value) is forbidden request-header if these steps return true:
auto name = StringView { header.name };
auto const& [name, value] = header;
// 1. If name is a byte-case-insensitive match for one of:
// [...]
@ -506,11 +468,11 @@ bool is_forbidden_request_header(Header const& header)
"X-HTTP-Method-Override"sv,
"X-Method-Override"sv)) {
// 1. Let parsedValues be the result of getting, decoding, and splitting value.
auto parsed_values = get_decode_and_split_header_value(header.value);
auto parsed_values = get_decode_and_split_header_value(value);
// 2. For each method of parsedValues: if the isomorphic encoding of method is a forbidden method, then return true.
// Note: The values returned from get_decode_and_split_header_value have already been decoded.
if (parsed_values.has_value() && any_of(*parsed_values, [](auto method) { return is_forbidden_method(method.bytes()); }))
// NB: The values returned from get_decode_and_split_header_value have already been decoded.
if (any_of(parsed_values, [](auto const& method) { return is_forbidden_method(method); }))
return true;
}
@ -519,12 +481,12 @@ bool is_forbidden_request_header(Header const& header)
}
// https://fetch.spec.whatwg.org/#forbidden-response-header-name
bool is_forbidden_response_header_name(ReadonlyBytes header_name)
bool is_forbidden_response_header_name(StringView header_name)
{
// A forbidden response-header name is a header name that is a byte-case-insensitive match for one of:
// - `Set-Cookie`
// - `Set-Cookie2`
return StringView { header_name }.is_one_of_ignoring_ascii_case(
return header_name.is_one_of_ignoring_ascii_case(
"Set-Cookie"sv,
"Set-Cookie2"sv);
}
@ -553,7 +515,7 @@ StringView legacy_extract_an_encoding(Optional<MimeSniff::MimeType> const& mime_
}
// https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes value)
Vector<String> get_decode_and_split_header_value(StringView value)
{
// To get, decode, and split a header value value, run these steps:
@ -571,7 +533,8 @@ Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes value)
// 5. While true:
while (true) {
// 1. Append the result of collecting a sequence of code points that are not U+0022 (") or U+002C (,) from input, given position, to temporaryValue.
// 1. Append the result of collecting a sequence of code points that are not U+0022 (") or U+002C (,) from
// input, given position, to temporaryValue.
// NOTE: The result might be the empty string.
temporary_value_builder.append(lexer.consume_until(is_any_of("\","sv)));
@ -607,34 +570,26 @@ Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes value)
}
// https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
OrderedHashTable<ByteBuffer> convert_header_names_to_a_sorted_lowercase_set(Span<ReadonlyBytes> header_names)
Vector<ByteString> convert_header_names_to_a_sorted_lowercase_set(ReadonlySpan<ByteString> header_names)
{
// To convert header names to a sorted-lowercase set, given a list of names headerNames, run these steps:
// 1. Let headerNamesSet be a new ordered set.
Vector<ByteBuffer> header_names_set;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen;
HashTable<StringView, CaseInsensitiveStringTraits> header_names_seen;
Vector<ByteString> header_names_set;
// 2. For each name of headerNames, append the result of byte-lowercasing name to headerNamesSet.
for (auto name : header_names) {
for (auto const& name : header_names) {
if (header_names_seen.contains(name))
continue;
auto bytes = MUST(ByteBuffer::copy(name));
Infra::byte_lowercase(bytes);
header_names_seen.set(name);
header_names_set.append(move(bytes));
header_names_set.append(name.to_lowercase());
}
// 3. Return the result of sorting headerNamesSet in ascending order with byte less than.
quick_sort(header_names_set, [](auto const& a, auto const& b) {
return StringView { a } < StringView { b };
});
OrderedHashTable<ByteBuffer> ordered { header_names_set.size() };
for (auto& name : header_names_set) {
auto result = ordered.set(move(name));
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}
return ordered;
quick_sort(header_names_set);
return header_names_set;
}
// https://fetch.spec.whatwg.org/#build-a-content-range
@ -651,7 +606,7 @@ ByteString build_content_range(u64 range_start, u64 range_end, u64 full_length)
}
// https://fetch.spec.whatwg.org/#simple-range-header-value
Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes const value, bool const allow_whitespace)
Optional<RangeHeaderValue> parse_single_range_header_value(StringView const value, bool const allow_whitespace)
{
// 1. Let data be the isomorphic decoding of value.
auto const data = Infra::isomorphic_decode(value);
@ -722,35 +677,32 @@ Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes const v
bool is_cors_safelisted_request_header(Header const& header)
{
// To determine whether a header (name, value) is a CORS-safelisted request-header, run these steps:
auto const& value = header.value;
auto const& [name, value] = header;
// 1. If values length is greater than 128, then return false.
if (value.size() > 128)
if (value.length() > 128)
return false;
// 2. Byte-lowercase name and switch on the result:
auto name = StringView { header.name };
// `accept`
if (name.equals_ignoring_ascii_case("accept"sv)) {
// If value contains a CORS-unsafe request-header byte, then return false.
if (any_of(value.span(), is_cors_unsafe_request_header_byte))
if (any_of(value, is_cors_unsafe_request_header_byte))
return false;
}
// `accept-language`
// `content-language`
else if (name.is_one_of_ignoring_ascii_case("accept-language"sv, "content-language"sv)) {
// If value contains a byte that is not in the range 0x30 (0) to 0x39 (9), inclusive, is not in the range 0x41 (A) to 0x5A (Z), inclusive, is not in the range 0x61 (a) to 0x7A (z), inclusive, and is not 0x20 (SP), 0x2A (*), 0x2C (,), 0x2D (-), 0x2E (.), 0x3B (;), or 0x3D (=), then return false.
if (any_of(value.span(), [](auto byte) {
return !(is_ascii_digit(byte) || is_ascii_alpha(byte) || " *,-.;="sv.contains(static_cast<char>(byte)));
if (any_of(value, [](auto byte) {
return !(is_ascii_digit(byte) || is_ascii_alpha(byte) || " *,-.;="sv.contains(byte));
}))
return false;
}
// `content-type`
else if (name.equals_ignoring_ascii_case("content-type"sv)) {
// 1. If value contains a CORS-unsafe request-header byte, then return false.
if (any_of(value.span(), is_cors_unsafe_request_header_byte))
if (any_of(value, is_cors_unsafe_request_header_byte))
return false;
// 2. Let mimeType be the result of parsing the result of isomorphic decoding value.
@ -800,15 +752,15 @@ bool is_cors_unsafe_request_header_byte(u8 byte)
}
// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-names
OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const& headers)
Vector<ByteString> get_cors_unsafe_header_names(HeaderList const& headers)
{
// The CORS-unsafe request-header names, given a header list headers, are determined as follows:
// 1. Let unsafeNames be a new list.
Vector<ReadonlyBytes> unsafe_names;
Vector<ByteString> unsafe_names;
// 2. Let potentiallyUnsafeNames be a new list.
Vector<ReadonlyBytes> potentially_unsafe_names;
Vector<ByteString> potentially_unsafe_names;
// 3. Let safelistValueSize be 0.
Checked<size_t> safelist_value_size = 0;
@ -817,42 +769,42 @@ OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const& head
for (auto const& header : headers) {
// 1. If header is not a CORS-safelisted request-header, then append headers name to unsafeNames.
if (!is_cors_safelisted_request_header(header)) {
unsafe_names.append(header.name.span());
unsafe_names.append(header.name);
}
// 2. Otherwise, append headers name to potentiallyUnsafeNames and increase safelistValueSize by headers values length.
// 2. Otherwise, append headers name to potentiallyUnsafeNames and increase safelistValueSize by headers
// values length.
else {
potentially_unsafe_names.append(header.name.span());
safelist_value_size += header.value.size();
potentially_unsafe_names.append(header.name);
safelist_value_size += header.value.length();
}
}
// 5. If safelistValueSize is greater than 1024, then for each name of potentiallyUnsafeNames, append name to unsafeNames.
if (safelist_value_size.has_overflow() || safelist_value_size.value() > 1024) {
for (auto const& name : potentially_unsafe_names)
unsafe_names.append(name);
}
// 5. If safelistValueSize is greater than 1024, then for each name of potentiallyUnsafeNames, append name to
// unsafeNames.
if (safelist_value_size.has_overflow() || safelist_value_size.value() > 1024)
unsafe_names.extend(move(potentially_unsafe_names));
// 6. Return the result of convert header names to a sorted-lowercase set with unsafeNames.
return convert_header_names_to_a_sorted_lowercase_set(unsafe_names.span());
return convert_header_names_to_a_sorted_lowercase_set(unsafe_names);
}
// https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
bool is_cors_non_wildcard_request_header_name(ReadonlyBytes header_name)
bool is_cors_non_wildcard_request_header_name(StringView header_name)
{
// A CORS non-wildcard request-header name is a header name that is a byte-case-insensitive match for `Authorization`.
return StringView { header_name }.equals_ignoring_ascii_case("Authorization"sv);
return header_name.equals_ignoring_ascii_case("Authorization"sv);
}
// https://fetch.spec.whatwg.org/#privileged-no-cors-request-header-name
bool is_privileged_no_cors_request_header_name(ReadonlyBytes header_name)
bool is_privileged_no_cors_request_header_name(StringView header_name)
{
// A privileged no-CORS request-header name is a header name that is a byte-case-insensitive match for one of
// - `Range`.
return StringView { header_name }.equals_ignoring_ascii_case("Range"sv);
return header_name.equals_ignoring_ascii_case("Range"sv);
}
// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
bool is_cors_safelisted_response_header_name(ReadonlyBytes header_name, Span<ReadonlyBytes> list)
bool is_cors_safelisted_response_header_name(StringView header_name, ReadonlySpan<StringView> list)
{
// A CORS-safelisted response-header name, given a list of header names list, is a header name that is a byte-case-insensitive match for one of
// - `Cache-Control`
@ -863,7 +815,7 @@ bool is_cors_safelisted_response_header_name(ReadonlyBytes header_name, Span<Rea
// - `Last-Modified`
// - `Pragma`
// - Any item in list that is not a forbidden response-header name.
return StringView { header_name }.is_one_of_ignoring_ascii_case(
return header_name.is_one_of_ignoring_ascii_case(
"Cache-Control"sv,
"Content-Language"sv,
"Content-Length"sv,
@ -872,20 +824,20 @@ bool is_cors_safelisted_response_header_name(ReadonlyBytes header_name, Span<Rea
"Last-Modified"sv,
"Pragma"sv)
|| any_of(list, [&](auto list_header_name) {
return StringView { header_name }.equals_ignoring_ascii_case(list_header_name)
return header_name.equals_ignoring_ascii_case(list_header_name)
&& !is_forbidden_response_header_name(list_header_name);
});
}
// https://fetch.spec.whatwg.org/#no-cors-safelisted-request-header-name
bool is_no_cors_safelisted_request_header_name(ReadonlyBytes header_name)
bool is_no_cors_safelisted_request_header_name(StringView header_name)
{
// A no-CORS-safelisted request-header name is a header name that is a byte-case-insensitive match for one of
// - `Accept`
// - `Accept-Language`
// - `Content-Language`
// - `Content-Type`
return StringView { header_name }.is_one_of_ignoring_ascii_case(
return header_name.is_one_of_ignoring_ascii_case(
"Accept"sv,
"Accept-Language"sv,
"Content-Language"sv,
@ -906,10 +858,11 @@ bool is_no_cors_safelisted_request_header(Header const& header)
}
// https://fetch.spec.whatwg.org/#default-user-agent-value
ByteBuffer default_user_agent_value()
ByteString const& default_user_agent_value()
{
// A default `User-Agent` value is an implementation-defined header value for the `User-Agent` header.
return MUST(ByteBuffer::copy(ResourceLoader::the().user_agent().bytes()));
static auto user_agent = ResourceLoader::the().user_agent().to_byte_string();
return user_agent;
}
}

View file

@ -6,10 +6,7 @@
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/HashTable.h>
#include <AK/ByteString.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Vector.h>
@ -25,14 +22,12 @@ namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-header
// A header is a tuple that consists of a name (a header name) and value (a header value).
struct WEB_API Header {
[[nodiscard]] static Header copy(Header const&);
[[nodiscard]] static Header from_string_pair(StringView, StringView);
[[nodiscard]] static Header from_latin1_pair(StringView, StringView);
[[nodiscard]] static Header isomorphic_encode(StringView, StringView);
[[nodiscard]] Optional<Vector<ByteBuffer>> extract_header_values() const;
Optional<Vector<ByteString>> extract_header_values() const;
ByteBuffer name;
ByteBuffer value;
ByteString name;
ByteString value;
};
// https://fetch.spec.whatwg.org/#concept-header-list
@ -51,24 +46,24 @@ public:
using Vector::end;
using Vector::is_empty;
[[nodiscard]] bool contains(ReadonlyBytes) const;
[[nodiscard]] Optional<ByteBuffer> get(ReadonlyBytes) const;
[[nodiscard]] Optional<Vector<String>> get_decode_and_split(ReadonlyBytes) const;
[[nodiscard]] bool contains(StringView) const;
[[nodiscard]] Optional<ByteString> get(StringView) const;
[[nodiscard]] Optional<Vector<String>> get_decode_and_split(StringView) const;
void append(Header);
void delete_(ReadonlyBytes name);
void delete_(StringView name);
void set(Header);
void combine(Header);
[[nodiscard]] Vector<Header> sort_and_combine() const;
struct ExtractHeaderParseFailure { };
[[nodiscard]] Variant<Empty, Vector<ByteBuffer>, ExtractHeaderParseFailure> extract_header_list_values(ReadonlyBytes) const;
[[nodiscard]] Variant<Empty, Vector<ByteString>, ExtractHeaderParseFailure> extract_header_list_values(StringView) const;
struct ExtractLengthFailure { };
[[nodiscard]] Variant<Empty, u64, ExtractLengthFailure> extract_length() const;
[[nodiscard]] Optional<MimeSniff::MimeType> extract_mime_type() const;
[[nodiscard]] Vector<ByteBuffer> unique_names() const;
[[nodiscard]] Vector<ByteString> unique_names() const;
};
struct RangeHeaderValue {
@ -76,29 +71,29 @@ struct RangeHeaderValue {
Optional<u64> end;
};
[[nodiscard]] bool is_header_name(ReadonlyBytes);
[[nodiscard]] bool is_header_value(ReadonlyBytes);
[[nodiscard]] ByteBuffer normalize_header_value(ReadonlyBytes);
[[nodiscard]] bool is_header_name(StringView);
[[nodiscard]] bool is_header_value(StringView);
[[nodiscard]] ByteString normalize_header_value(StringView);
[[nodiscard]] bool is_forbidden_request_header(Header const&);
[[nodiscard]] bool is_forbidden_response_header_name(ReadonlyBytes);
[[nodiscard]] bool is_forbidden_response_header_name(StringView);
[[nodiscard]] WEB_API StringView legacy_extract_an_encoding(Optional<MimeSniff::MimeType> const& mime_type, StringView fallback_encoding);
[[nodiscard]] Optional<Vector<String>> get_decode_and_split_header_value(ReadonlyBytes);
[[nodiscard]] OrderedHashTable<ByteBuffer> convert_header_names_to_a_sorted_lowercase_set(Span<ReadonlyBytes>);
[[nodiscard]] Vector<String> get_decode_and_split_header_value(StringView);
[[nodiscard]] Vector<ByteString> convert_header_names_to_a_sorted_lowercase_set(ReadonlySpan<ByteString>);
[[nodiscard]] WEB_API ByteString build_content_range(u64 range_start, u64 range_end, u64 full_length);
[[nodiscard]] WEB_API Optional<RangeHeaderValue> parse_single_range_header_value(ReadonlyBytes, bool);
[[nodiscard]] WEB_API Optional<RangeHeaderValue> parse_single_range_header_value(StringView, bool);
[[nodiscard]] bool is_cors_safelisted_request_header(Header const&);
[[nodiscard]] bool is_cors_unsafe_request_header_byte(u8);
[[nodiscard]] WEB_API OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const&);
[[nodiscard]] WEB_API bool is_cors_non_wildcard_request_header_name(ReadonlyBytes);
[[nodiscard]] bool is_privileged_no_cors_request_header_name(ReadonlyBytes);
[[nodiscard]] bool is_cors_safelisted_response_header_name(ReadonlyBytes, Span<ReadonlyBytes>);
[[nodiscard]] bool is_no_cors_safelisted_request_header_name(ReadonlyBytes);
[[nodiscard]] WEB_API Vector<ByteString> get_cors_unsafe_header_names(HeaderList const&);
[[nodiscard]] WEB_API bool is_cors_non_wildcard_request_header_name(StringView);
[[nodiscard]] bool is_privileged_no_cors_request_header_name(StringView);
[[nodiscard]] bool is_cors_safelisted_response_header_name(StringView, ReadonlySpan<StringView>);
[[nodiscard]] bool is_no_cors_safelisted_request_header_name(StringView);
[[nodiscard]] bool is_no_cors_safelisted_request_header(Header const&);
[[nodiscard]] WEB_API ByteBuffer default_user_agent_value();
[[nodiscard]] WEB_API ByteString const& default_user_agent_value();
}

View file

@ -5,44 +5,46 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ByteBuffer.h>
#include <AK/StringView.h>
#include <LibRegex/Regex.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h>
#include <LibWeb/Infra/ByteSequences.h>
namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-method
bool is_method(ReadonlyBytes method)
bool is_method(StringView method)
{
// A method is a byte sequence that matches the method token production.
Regex<ECMA262Parser> regex { R"~~~(^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$)~~~" };
return regex.has_match(StringView { method });
return regex.has_match(method);
}
// https://fetch.spec.whatwg.org/#cors-safelisted-method
bool is_cors_safelisted_method(ReadonlyBytes method)
bool is_cors_safelisted_method(StringView method)
{
// A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`.
return StringView { method }.is_one_of("GET"sv, "HEAD"sv, "POST"sv);
return method.is_one_of("GET"sv, "HEAD"sv, "POST"sv);
}
// https://fetch.spec.whatwg.org/#forbidden-method
bool is_forbidden_method(ReadonlyBytes method)
bool is_forbidden_method(StringView method)
{
// A forbidden method is a method that is a byte-case-insensitive match for `CONNECT`, `TRACE`, or `TRACK`.
return StringView { method }.is_one_of_ignoring_ascii_case("CONNECT"sv, "TRACE"sv, "TRACK"sv);
return method.is_one_of_ignoring_ascii_case("CONNECT"sv, "TRACE"sv, "TRACK"sv);
}
// https://fetch.spec.whatwg.org/#concept-method-normalize
ByteBuffer normalize_method(ReadonlyBytes method)
ByteString normalize_method(StringView method)
{
// To normalize a method, if it is a byte-case-insensitive match for `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`, or `PUT`, byte-uppercase it.
auto bytes = MUST(ByteBuffer::copy(method));
if (StringView { method }.is_one_of_ignoring_ascii_case("DELETE"sv, "GET"sv, "HEAD"sv, "OPTIONS"sv, "POST"sv, "PUT"sv))
Infra::byte_uppercase(bytes);
return bytes;
// To normalize a method, if it is a byte-case-insensitive match for `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`,
// or `PUT`, byte-uppercase it.
static auto NORMALIZED_METHODS = to_array<ByteString>({ "DELETE"sv, "GET"sv, "HEAD"sv, "OPTIONS"sv, "POST"sv, "PUT"sv });
for (auto const& normalized_method : NORMALIZED_METHODS) {
if (normalized_method.equals_ignoring_ascii_case(method))
return normalized_method;
}
return method;
}
}

View file

@ -6,14 +6,15 @@
#pragma once
#include <AK/Forward.h>
#include <AK/ByteString.h>
#include <AK/StringView.h>
#include <LibWeb/Export.h>
namespace Web::Fetch::Infrastructure {
[[nodiscard]] bool is_method(ReadonlyBytes);
[[nodiscard]] WEB_API bool is_cors_safelisted_method(ReadonlyBytes);
[[nodiscard]] bool is_forbidden_method(ReadonlyBytes);
[[nodiscard]] ByteBuffer normalize_method(ReadonlyBytes);
[[nodiscard]] bool is_method(StringView);
[[nodiscard]] WEB_API bool is_cors_safelisted_method(StringView);
[[nodiscard]] bool is_forbidden_method(StringView);
[[nodiscard]] ByteString normalize_method(StringView);
}

View file

@ -14,6 +14,7 @@
#include <LibWeb/Fetch/Fetching/PendingResponse.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Infra/Strings.h>
namespace Web::Fetch::Infrastructure {
@ -212,11 +213,11 @@ String Request::serialize_origin() const
}
// https://fetch.spec.whatwg.org/#byte-serializing-a-request-origin
ByteBuffer Request::byte_serialize_origin() const
ByteString Request::byte_serialize_origin() const
{
// Byte-serializing a request origin, given a request request, is to return the result of serializing a request origin with request, isomorphic encoded.
auto serialized_origin = serialize_origin();
return MUST(ByteBuffer::copy(serialized_origin.bytes()));
// Byte-serializing a request origin, given a request request, is to return the result of serializing a request
// origin with request, isomorphic encoded.
return Infra::isomorphic_encode(serialize_origin());
}
// https://fetch.spec.whatwg.org/#concept-request-clone
@ -283,27 +284,15 @@ void Request::add_range_header(u64 first, Optional<u64> const& last)
VERIFY(!last.has_value() || first <= last.value());
// 2. Let rangeValue be `bytes=`.
auto range_value = MUST(ByteBuffer::copy("bytes"sv.bytes()));
// 3. Serialize and isomorphic encode first, and append the result to rangeValue.
auto serialized_first = String::number(first);
range_value.append(serialized_first.bytes());
// 4. Append 0x2D (-) to rangeValue.
range_value.append('-');
// 5. If last is given, then serialize and isomorphic encode it, and append the result to rangeValue.
if (last.has_value()) {
auto serialized_last = String::number(*last);
range_value.append(serialized_last.bytes());
}
auto range_value = last.has_value()
? ByteString::formatted("bytes={}-{}", first, *last)
: ByteString::formatted("bytes={}-", first);
// 6. Append (`Range`, rangeValue) to requests header list.
auto header = Header {
.name = MUST(ByteBuffer::copy("Range"sv.bytes())),
.value = move(range_value),
};
m_header_list->append(move(header));
m_header_list->append({ "Range"sv, move(range_value) });
}
// https://fetch.spec.whatwg.org/#append-a-request-origin-header
@ -314,21 +303,17 @@ void Request::add_origin_header()
// 2. If requests response tainting is "cors" or requests mode is "websocket", then append (`Origin`, serializedOrigin) to requests header list.
if (m_response_tainting == ResponseTainting::CORS || m_mode == Mode::WebSocket) {
auto header = Header {
.name = MUST(ByteBuffer::copy("Origin"sv.bytes())),
.value = move(serialized_origin),
};
m_header_list->append(move(header));
m_header_list->append({ "Origin"sv, move(serialized_origin) });
}
// 3. Otherwise, if requests method is neither `GET` nor `HEAD`, then:
else if (!StringView { m_method }.is_one_of("GET"sv, "HEAD"sv)) {
else if (!m_method.is_one_of("GET"sv, "HEAD"sv)) {
// 1. If requests mode is not "cors", then switch on requests referrer policy:
if (m_mode != Mode::CORS) {
switch (m_referrer_policy) {
// -> "no-referrer"
case ReferrerPolicy::ReferrerPolicy::NoReferrer:
// Set serializedOrigin to `null`.
serialized_origin = MUST(ByteBuffer::copy("null"sv.bytes()));
serialized_origin = "null"sv;
break;
// -> "no-referrer-when-downgrade"
// -> "strict-origin"
@ -339,14 +324,14 @@ void Request::add_origin_header()
// If requests origin is a tuple origin, its scheme is "https", and requests current URLs scheme is
// not "https", then set serializedOrigin to `null`.
if (m_origin.has<URL::Origin>() && !m_origin.get<URL::Origin>().is_opaque() && m_origin.get<URL::Origin>().scheme() == "https"sv && current_url().scheme() != "https"sv)
serialized_origin = MUST(ByteBuffer::copy("null"sv.bytes()));
serialized_origin = "null"sv;
break;
// -> "same-origin"
case ReferrerPolicy::ReferrerPolicy::SameOrigin:
// If requests origin is not same origin with requests current URLs origin, then set serializedOrigin
// to `null`.
if (m_origin.has<URL::Origin>() && !m_origin.get<URL::Origin>().is_same_origin(current_url().origin()))
serialized_origin = MUST(ByteBuffer::copy("null"sv.bytes()));
serialized_origin = "null"sv;
break;
// -> Otherwise
default:
@ -356,11 +341,7 @@ void Request::add_origin_header()
}
// 2. Append (`Origin`, serializedOrigin) to requests header list.
auto header = Header {
.name = MUST(ByteBuffer::copy("Origin"sv.bytes())),
.value = move(serialized_origin),
};
m_header_list->append(move(header));
m_header_list->append({ "Origin"sv, move(serialized_origin) });
}
}

View file

@ -8,6 +8,7 @@
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/Optional.h>
@ -171,8 +172,8 @@ public:
[[nodiscard]] static GC::Ref<Request> create(JS::VM&);
[[nodiscard]] ReadonlyBytes method() const LIFETIME_BOUND { return m_method; }
void set_method(ByteBuffer method) { m_method = move(method); }
[[nodiscard]] ByteString const& method() const { return m_method; }
void set_method(ByteString method) { m_method = move(method); }
[[nodiscard]] bool local_urls_only() const { return m_local_urls_only; }
void set_local_urls_only(bool local_urls_only) { m_local_urls_only = local_urls_only; }
@ -307,7 +308,7 @@ public:
[[nodiscard]] RedirectTaint redirect_taint() const;
[[nodiscard]] String serialize_origin() const;
[[nodiscard]] ByteBuffer byte_serialize_origin() const;
[[nodiscard]] ByteString byte_serialize_origin() const;
[[nodiscard]] GC::Ref<Request> clone(JS::Realm&) const;
@ -335,7 +336,7 @@ private:
// https://fetch.spec.whatwg.org/#concept-request-method
// A request has an associated method (a method). Unless stated otherwise it is `GET`.
ByteBuffer m_method { ByteBuffer::copy("GET"sv.bytes()).release_value() };
ByteString m_method { "GET"sv };
// https://fetch.spec.whatwg.org/#local-urls-only-flag
// A request has an associated local-URLs-only flag. Unless stated otherwise it is unset.

View file

@ -125,8 +125,8 @@ ErrorOr<Optional<URL::URL>> Response::location_url(Optional<String> const& reque
return Optional<URL::URL> {};
// 2. Let location be the result of extracting header list values given `Location` and responses header list.
auto location_values_or_failure = m_header_list->extract_header_list_values("Location"sv.bytes());
auto const* location_values = location_values_or_failure.get_pointer<Vector<ByteBuffer>>();
auto location_values_or_failure = m_header_list->extract_header_list_values("Location"sv);
auto const* location_values = location_values_or_failure.get_pointer<Vector<ByteString>>();
if (!location_values || location_values->size() != 1)
return OptionalNone {};
@ -246,8 +246,8 @@ u64 Response::current_age() const
{
// The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic operation; or 0, if not available.
Optional<AK::Duration> age;
if (auto const age_header = header_list()->get("Age"sv.bytes()); age_header.has_value()) {
if (auto converted_age = StringView { *age_header }.to_number<u64>(); converted_age.has_value())
if (auto const age_header = header_list()->get("Age"sv); age_header.has_value()) {
if (auto converted_age = age_header->to_number<u64>(); converted_age.has_value())
age = AK::Duration::from_seconds(converted_age.value());
}
@ -281,7 +281,7 @@ u64 Response::current_age() const
// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
u64 Response::freshness_lifetime() const
{
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv.bytes());
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv);
if (!elem.has_value())
return 0;
@ -314,7 +314,7 @@ u64 Response::freshness_lifetime() const
// https://httpwg.org/specs/rfc5861.html#n-the-stale-while-revalidate-cache-control-extension
u64 Response::stale_while_revalidate_lifetime() const
{
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv.bytes());
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv);
if (!elem.has_value())
return 0;
@ -386,9 +386,11 @@ GC::Ref<CORSFilteredResponse> CORSFilteredResponse::create(JS::VM& vm, GC::Ref<R
// A CORS filtered response is a filtered response whose type is "cors" and header list excludes
// any headers in internal responses header list whose name is not a CORS-safelisted response-header
// name, given internal responses CORS-exposed header-name list.
Vector<ReadonlyBytes> cors_exposed_header_name_list;
Vector<StringView> cors_exposed_header_name_list;
cors_exposed_header_name_list.ensure_capacity(internal_response->cors_exposed_header_name_list().size());
for (auto const& header_name : internal_response->cors_exposed_header_name_list())
cors_exposed_header_name_list.append(header_name.span());
cors_exposed_header_name_list.unchecked_append(header_name);
auto header_list = HeaderList::create(vm);
for (auto const& header : *internal_response->header_list()) {

View file

@ -6,7 +6,7 @@
#pragma once
#include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/Optional.h>
@ -78,8 +78,8 @@ public:
[[nodiscard]] virtual Status status() const { return m_status; }
virtual void set_status(Status status) { m_status = status; }
[[nodiscard]] virtual ReadonlyBytes status_message() const LIFETIME_BOUND { return m_status_message; }
virtual void set_status_message(ByteBuffer status_message) { m_status_message = move(status_message); }
[[nodiscard]] virtual ByteString const& status_message() const { return m_status_message; }
virtual void set_status_message(ByteString status_message) { m_status_message = move(status_message); }
[[nodiscard]] virtual GC::Ref<HeaderList> header_list() const { return m_header_list; }
virtual void set_header_list(GC::Ref<HeaderList> header_list) { m_header_list = header_list; }
@ -90,8 +90,8 @@ public:
[[nodiscard]] virtual Optional<CacheState> const& cache_state() const { return m_cache_state; }
virtual void set_cache_state(Optional<CacheState> cache_state) { m_cache_state = move(cache_state); }
[[nodiscard]] virtual Vector<ByteBuffer> const& cors_exposed_header_name_list() const { return m_cors_exposed_header_name_list; }
virtual void set_cors_exposed_header_name_list(Vector<ByteBuffer> cors_exposed_header_name_list) { m_cors_exposed_header_name_list = move(cors_exposed_header_name_list); }
[[nodiscard]] virtual Vector<ByteString> const& cors_exposed_header_name_list() const { return m_cors_exposed_header_name_list; }
virtual void set_cors_exposed_header_name_list(Vector<ByteString> cors_exposed_header_name_list) { m_cors_exposed_header_name_list = move(cors_exposed_header_name_list); }
[[nodiscard]] virtual bool range_requested() const { return m_range_requested; }
virtual void set_range_requested(bool range_requested) { m_range_requested = range_requested; }
@ -103,10 +103,10 @@ public:
virtual void set_timing_allow_passed(bool timing_allow_passed) { m_timing_allow_passed = timing_allow_passed; }
[[nodiscard]] virtual BodyInfo const& body_info() const { return m_body_info; }
virtual void set_body_info(BodyInfo body_info) { m_body_info = body_info; }
virtual void set_body_info(BodyInfo body_info) { m_body_info = move(body_info); }
[[nodiscard]] RedirectTaint redirect_taint() const { return m_redirect_taint; }
void set_redirect_taint(RedirectTaint redirect_taint) { m_redirect_taint = move(redirect_taint); }
void set_redirect_taint(RedirectTaint redirect_taint) { m_redirect_taint = redirect_taint; }
[[nodiscard]] bool is_aborted_network_error() const;
[[nodiscard]] bool is_network_error() const;
@ -153,7 +153,7 @@ private:
// https://fetch.spec.whatwg.org/#concept-response-status-message
// A response has an associated status message. Unless stated otherwise it is the empty byte sequence.
ByteBuffer m_status_message;
ByteString m_status_message;
// https://fetch.spec.whatwg.org/#concept-response-header-list
// A response has an associated header list (a header list). Unless stated otherwise it is empty.
@ -169,7 +169,7 @@ private:
// https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list
// A response has an associated CORS-exposed header-name list (a list of zero or more header names). The list is empty unless otherwise specified.
Vector<ByteBuffer> m_cors_exposed_header_name_list;
Vector<ByteString> m_cors_exposed_header_name_list;
// https://fetch.spec.whatwg.org/#concept-response-range-requested-flag
// A response has an associated range-requested flag, which is initially unset.
@ -200,14 +200,14 @@ private:
u64 stale_while_revalidate_lifetime() const;
// Non-standard
ByteBuffer m_method;
ByteString m_method;
MonotonicTime m_response_time;
Optional<String> m_network_error_message;
public:
[[nodiscard]] ByteBuffer const& method() const { return m_method; }
void set_method(ByteBuffer method) { m_method = move(method); }
[[nodiscard]] ByteString const& method() const { return m_method; }
void set_method(ByteString method) { m_method = move(method); }
};
// https://fetch.spec.whatwg.org/#concept-filtered-response
@ -230,8 +230,8 @@ public:
[[nodiscard]] virtual Status status() const override { return m_internal_response->status(); }
virtual void set_status(Status status) override { m_internal_response->set_status(status); }
[[nodiscard]] virtual ReadonlyBytes status_message() const LIFETIME_BOUND override { return m_internal_response->status_message(); }
virtual void set_status_message(ByteBuffer status_message) override { m_internal_response->set_status_message(move(status_message)); }
[[nodiscard]] virtual ByteString const& status_message() const override { return m_internal_response->status_message(); }
virtual void set_status_message(ByteString status_message) override { m_internal_response->set_status_message(move(status_message)); }
[[nodiscard]] virtual GC::Ref<HeaderList> header_list() const override { return m_internal_response->header_list(); }
virtual void set_header_list(GC::Ref<HeaderList> header_list) override { m_internal_response->set_header_list(header_list); }
@ -242,8 +242,8 @@ public:
[[nodiscard]] virtual Optional<CacheState> const& cache_state() const override { return m_internal_response->cache_state(); }
virtual void set_cache_state(Optional<CacheState> cache_state) override { m_internal_response->set_cache_state(move(cache_state)); }
[[nodiscard]] virtual Vector<ByteBuffer> const& cors_exposed_header_name_list() const override { return m_internal_response->cors_exposed_header_name_list(); }
virtual void set_cors_exposed_header_name_list(Vector<ByteBuffer> cors_exposed_header_name_list) override { m_internal_response->set_cors_exposed_header_name_list(move(cors_exposed_header_name_list)); }
[[nodiscard]] virtual Vector<ByteString> const& cors_exposed_header_name_list() const override { return m_internal_response->cors_exposed_header_name_list(); }
virtual void set_cors_exposed_header_name_list(Vector<ByteString> cors_exposed_header_name_list) override { m_internal_response->set_cors_exposed_header_name_list(move(cors_exposed_header_name_list)); }
[[nodiscard]] virtual bool range_requested() const override { return m_internal_response->range_requested(); }
virtual void set_range_requested(bool range_requested) override { m_internal_response->set_range_requested(range_requested); }
@ -317,7 +317,7 @@ public:
[[nodiscard]] virtual Vector<URL::URL> const& url_list() const override { return m_url_list; }
[[nodiscard]] virtual Vector<URL::URL>& url_list() override { return m_url_list; }
[[nodiscard]] virtual Status status() const override { return 0; }
[[nodiscard]] virtual ReadonlyBytes status_message() const LIFETIME_BOUND override { return {}; }
[[nodiscard]] virtual ByteString const& status_message() const override { return m_method; }
[[nodiscard]] virtual GC::Ref<HeaderList> header_list() const override { return m_header_list; }
[[nodiscard]] virtual GC::Ptr<Body> body() const override { return nullptr; }
@ -327,6 +327,7 @@ private:
virtual void visit_edges(JS::Cell::Visitor&) override;
Vector<URL::URL> m_url_list;
ByteString const m_method;
GC::Ref<HeaderList> m_header_list;
};
@ -340,7 +341,7 @@ public:
[[nodiscard]] virtual Type type() const override { return Type::OpaqueRedirect; }
[[nodiscard]] virtual Status status() const override { return 0; }
[[nodiscard]] virtual ReadonlyBytes status_message() const LIFETIME_BOUND override { return {}; }
[[nodiscard]] virtual ByteString const& status_message() const override { return m_method; }
[[nodiscard]] virtual GC::Ref<HeaderList> header_list() const override { return m_header_list; }
[[nodiscard]] virtual GC::Ptr<Body> body() const override { return nullptr; }
@ -349,6 +350,7 @@ private:
virtual void visit_edges(JS::Cell::Visitor&) override;
ByteString const m_method;
GC::Ref<HeaderList> m_header_list;
};

View file

@ -183,7 +183,7 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
// method
// requests method.
request->set_method(MUST(ByteBuffer::copy(input_request->method())));
request->set_method(input_request->method());
// header list
// A copy of requests header list.
@ -369,16 +369,16 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
auto method = *init.method;
// 2. If method is not a method or method is a forbidden method, then throw a TypeError.
if (!Infrastructure::is_method(method.bytes()))
if (!Infrastructure::is_method(method))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Method has invalid value"sv };
if (Infrastructure::is_forbidden_method(method.bytes()))
if (Infrastructure::is_forbidden_method(method))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Method must not be one of CONNECT, TRACE, or TRACK"sv };
// 3. Normalize method.
method = MUST(String::from_utf8(Infrastructure::normalize_method(method.bytes())));
auto normalized_method = Infrastructure::normalize_method(method);
// 4. Set requests method to method.
request->set_method(MUST(ByteBuffer::copy(method.bytes())));
request->set_method(move(normalized_method));
}
// 26. If init["signal"] exists, then set signal to it.
@ -431,7 +431,7 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
// 4. If headers is a Headers object, then for each header of its header list, append header to thiss headers.
if (auto* header_list = headers.get_pointer<GC::Ref<Infrastructure::HeaderList>>()) {
for (auto& header : *header_list->ptr())
TRY(request_object->headers()->append(Infrastructure::Header::from_string_pair(header.name, header.value)));
TRY(request_object->headers()->append(Infrastructure::Header::isomorphic_encode(header.name, header.value)));
}
// 5. Otherwise, fill thiss headers with headers.
else {
@ -445,7 +445,7 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
input_body = input.get<GC::Root<Request>>()->request()->body();
// 35. If either init["body"] exists and is non-null or inputBody is non-null, and requests method is `GET` or `HEAD`, then throw a TypeError.
if (((init.body.has_value() && (*init.body).has_value()) || (input_body.has_value() && !input_body.value().has<Empty>())) && StringView { request->method() }.is_one_of("GET"sv, "HEAD"sv))
if (((init.body.has_value() && (*init.body).has_value()) || (input_body.has_value() && !input_body.value().has<Empty>())) && request->method().is_one_of("GET"sv, "HEAD"sv))
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Method must not be GET or HEAD when body is provided"sv };
// 36. Let initBody be null.
@ -463,8 +463,8 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
auto const& type = body_with_type.type;
// 4. If type is non-null and thiss headerss header list does not contain `Content-Type`, then append (`Content-Type`, type) to thiss headers.
if (type.has_value() && !request_object->headers()->header_list()->contains("Content-Type"sv.bytes()))
TRY(request_object->headers()->append(Infrastructure::Header::from_string_pair("Content-Type"sv, type->span())));
if (type.has_value() && !request_object->headers()->header_list()->contains("Content-Type"sv))
TRY(request_object->headers()->append(Infrastructure::Header::isomorphic_encode("Content-Type"sv, *type)));
}
// 38. Let inputOrInitBody be initBody if it is non-null; otherwise inputBody.
@ -508,7 +508,7 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
String Request::method() const
{
// The method getter steps are to return thiss requests method.
return MUST(String::from_utf8(m_request->method()));
return MUST(String::from_byte_string(m_request->method()));
}
// https://fetch.spec.whatwg.org/#dom-request-url

View file

@ -111,7 +111,7 @@ WebIDL::ExceptionOr<void> Response::initialize_response(ResponseInit const& init
m_response->set_status(init.status);
// 4. Set responses responses status message to init["statusText"].
m_response->set_status_message(MUST(ByteBuffer::copy(init.status_text.bytes())));
m_response->set_status_message(init.status_text.to_byte_string());
// 5. If init["headers"] exists, then fill responses headers with init["headers"].
if (init.headers.has_value())
@ -127,13 +127,8 @@ WebIDL::ExceptionOr<void> Response::initialize_response(ResponseInit const& init
m_response->set_body(body->body);
// 3. If bodys type is non-null and responses header list does not contain `Content-Type`, then append (`Content-Type`, bodys type) to responses header list.
if (body->type.has_value() && !m_response->header_list()->contains("Content-Type"sv.bytes())) {
auto header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy("Content-Type"sv.bytes())),
.value = MUST(ByteBuffer::copy(body->type->span())),
};
m_response->header_list()->append(move(header));
}
if (body->type.has_value() && !m_response->header_list()->contains("Content-Type"sv))
m_response->header_list()->append({ "Content-Type"sv, *body->type });
}
return {};
@ -204,7 +199,7 @@ WebIDL::ExceptionOr<GC::Ref<Response>> Response::redirect(JS::VM& vm, String con
auto value = parsed_url->serialize();
// 7. Append (`Location`, value) to responseObjects responses header list.
auto header = Infrastructure::Header::from_string_pair("Location"sv, value);
auto header = Infrastructure::Header::isomorphic_encode("Location"sv, value);
response_object->response()->header_list()->append(move(header));
// 8. Return responseObject.
@ -229,7 +224,7 @@ WebIDL::ExceptionOr<GC::Ref<Response>> Response::json(JS::VM& vm, JS::Value data
// 4. Perform initialize a response given responseObject, init, and (body, "application/json").
auto body_with_type = Infrastructure::BodyWithType {
.body = body,
.type = MUST(ByteBuffer::copy("application/json"sv.bytes()))
.type = "application/json"sv,
};
TRY(response_object->initialize_response(init, move(body_with_type)));
@ -278,7 +273,7 @@ bool Response::ok() const
String Response::status_text() const
{
// The statusText getter steps are to return thiss responses status message.
return MUST(String::from_utf8(m_response->status_message()));
return MUST(String::from_byte_string(m_response->status_message()));
}
// https://fetch.spec.whatwg.org/#dom-response-headers

View file

@ -69,8 +69,7 @@ WebIDL::ExceptionOr<GC::Ref<EventSource>> EventSource::construct_impl(JS::Realm&
request->set_client(&settings);
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list.
auto header = Fetch::Infrastructure::Header::from_string_pair("Accept"sv, "text/event-stream"sv);
request->header_list()->set(move(header));
request->header_list()->set({ "Accept"sv, "text/event-stream"sv });
// 11. Set request's cache mode to "no-store".
request->set_cache_mode(Fetch::Infrastructure::Request::CacheMode::NoStore);
@ -319,8 +318,8 @@ void EventSource::reestablish_the_connection()
if (!m_last_event_id.is_empty()) {
// 1. Let lastEventIDValue be the EventSource object's last event ID string, encoded as UTF-8.
// 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header list.
auto header = Fetch::Infrastructure::Header::from_string_pair("Last-Event-ID"sv, m_last_event_id);
request->header_list()->set(header);
auto header = Fetch::Infrastructure::Header::isomorphic_encode("Last-Event-ID"sv, m_last_event_id);
request->header_list()->set(move(header));
}
// 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section.

View file

@ -794,10 +794,7 @@ static GC::Ref<NavigationParams> create_navigation_params_from_a_srcdoc_resource
// body: the UTF-8 encoding of documentResource, as a body
auto response = Fetch::Infrastructure::Response::create(vm);
response->url_list().append(URL::about_srcdoc());
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html"sv);
response->header_list()->append(move(header));
response->header_list()->append({ "Content-Type"sv, "text/html"sv });
response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, document_resource.get<String>().bytes()));
// 3. Let responseOrigin be the result of determining the origin given response's URL, targetSnapshotParams's sandboxing flags, and entry's document state's origin.
@ -1128,7 +1125,7 @@ static void create_navigation_params_by_fetching(GC::Ptr<SessionHistoryEntry> en
// 6. If documentResource is a POST resource:
if (auto* post_resource = document_resource.get_pointer<POSTResource>()) {
// 1. Set request's method to `POST`.
request->set_method(MUST(ByteBuffer::copy("POST"sv.bytes())));
request->set_method("POST"sv);
// 2. Set request's body to documentResource's request body.
request->set_body(document_resource.get<POSTResource>().request_body.value());
@ -1157,7 +1154,7 @@ static void create_navigation_params_by_fetching(GC::Ptr<SessionHistoryEntry> en
request_content_type = request_content_type_buffer.string_view();
}
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, request_content_type);
auto header = Fetch::Infrastructure::Header::isomorphic_encode("Content-Type"sv, request_content_type);
request->header_list()->append(move(header));
}
@ -2043,10 +2040,7 @@ GC::Ptr<DOM::Document> Navigable::evaluate_javascript_url(URL::URL const& url, U
// body: the UTF-8 encoding of result, as a body
auto response = Fetch::Infrastructure::Response::create(vm);
response->url_list().append(active_document()->url());
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html"sv);
response->header_list()->append(move(header));
response->header_list()->append({ "Content-Type"sv, "text/html"sv });
response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, result.bytes()));
// 12. Let policyContainer be targetNavigable's active document's policy container.

View file

@ -61,12 +61,12 @@ WebIDL::ExceptionOr<bool> NavigatorBeaconPartial::send_beacon(String const& url,
cors_mode = Fetch::Infrastructure::Request::Mode::CORS;
// If contentType value is a CORS-safelisted request-header value for the Content-Type header, set corsMode to "no-cors".
auto content_type_header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, content_type.value());
auto content_type_header = Fetch::Infrastructure::Header::isomorphic_encode("Content-Type"sv, content_type.value());
if (Fetch::Infrastructure::is_cors_safelisted_request_header(content_type_header))
cors_mode = Fetch::Infrastructure::Request::Mode::NoCORS;
// Append a Content-Type header with value contentType to headerList.
header_list->append(content_type_header);
header_list->append(move(content_type_header));
}
}
@ -74,12 +74,12 @@ WebIDL::ExceptionOr<bool> NavigatorBeaconPartial::send_beacon(String const& url,
// 7.1 Let req be a new request, initialized as follows:
auto req = Fetch::Infrastructure::Request::create(vm);
req->set_method(MUST(ByteBuffer::copy("POST"sv.bytes()))); // method: POST
req->set_client(&relevant_settings_object); // client: this's relevant settings object
req->set_url_list({ parsed_url.release_value() }); // url: parsedUrl
req->set_header_list(header_list); // header list: headerList
req->set_origin(origin); // origin: origin
req->set_keepalive(true); // keepalive: true
req->set_method("POST"sv); // method: POST
req->set_client(&relevant_settings_object); // client: this's relevant settings object
req->set_url_list({ parsed_url.release_value() }); // url: parsedUrl
req->set_header_list(header_list); // header list: headerList
req->set_origin(origin); // origin: origin
req->set_keepalive(true); // keepalive: true
if (transmitted_data)
req->set_body(GC::Ref<Fetch::Infrastructure::Body> { *transmitted_data }); // body: transmittedData
req->set_mode(cors_mode); // mode: corsMode

View file

@ -157,31 +157,35 @@ ErrorOr<String> convert_to_scalar_value_string(StringView string)
}
// https://infra.spec.whatwg.org/#isomorphic-encode
ByteBuffer isomorphic_encode(StringView input)
ByteString isomorphic_encode(StringView input)
{
// To isomorphic encode an isomorphic string input: return a byte sequence whose length is equal to inputs code
// point length and whose bytes have the same values as the values of inputs code points, in the same order.
// NOTE: This is essentially spec-speak for "Encode as ISO-8859-1 / Latin-1".
ByteBuffer buf = {};
// NB: This is essentially spec-speak for "Encode as ISO-8859-1 / Latin-1".
StringBuilder builder(input.length());
for (auto code_point : Utf8View { input }) {
// VERIFY(code_point <= 0xFF);
if (code_point > 0xFF)
dbgln("FIXME: Trying to isomorphic encode a string with code points > U+00FF.");
buf.append((u8)code_point);
builder.append(static_cast<u8>(code_point));
}
return buf;
return builder.to_byte_string();
}
// https://infra.spec.whatwg.org/#isomorphic-decode
String isomorphic_decode(ReadonlyBytes input)
String isomorphic_decode(StringView input)
{
// To isomorphic decode a byte sequence input, return a string whose code point length is equal
// to inputs length and whose code points have the same values as the values of inputs bytes, in the same order.
// NOTE: This is essentially spec-speak for "Decode as ISO-8859-1 / Latin-1".
StringBuilder builder(input.size());
for (u8 code_point : input) {
builder.append_code_point(code_point);
}
// To isomorphic decode a byte sequence input, return a string whose code point length is equal to inputs length
// and whose code points have the same values as the values of inputs bytes, in the same order.
// NB: This is essentially spec-speak for "Decode as ISO-8859-1 / Latin-1".
StringBuilder builder(input.length());
for (auto byte : input.bytes())
builder.append_code_point(byte);
return builder.to_string_without_validation();
}

View file

@ -20,8 +20,8 @@ WEB_API ErrorOr<String> strip_and_collapse_whitespace(StringView string);
Utf16String strip_and_collapse_whitespace(Utf16String const& string);
WEB_API bool is_code_unit_prefix(StringView potential_prefix, StringView input);
WEB_API ErrorOr<String> convert_to_scalar_value_string(StringView string);
ByteBuffer isomorphic_encode(StringView input);
WEB_API String isomorphic_decode(ReadonlyBytes input);
ByteString isomorphic_encode(StringView input);
WEB_API String isomorphic_decode(StringView input);
bool code_unit_less_than(StringView a, StringView b);
}

View file

@ -22,13 +22,13 @@ namespace Web::ReferrerPolicy {
ReferrerPolicy parse_a_referrer_policy_from_a_referrer_policy_header(Fetch::Infrastructure::Response const& response)
{
// 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and responses header list.
auto policy_tokens_or_failure = response.header_list()->extract_header_list_values("Referrer-Policy"sv.bytes());
auto policy_tokens_or_failure = response.header_list()->extract_header_list_values("Referrer-Policy"sv);
// 2. Let policy be the empty string.
auto policy = ReferrerPolicy::EmptyString;
// 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
if (auto const* policy_tokens = policy_tokens_or_failure.get_pointer<Vector<ByteBuffer>>()) {
if (auto const* policy_tokens = policy_tokens_or_failure.get_pointer<Vector<ByteString>>()) {
for (auto const& token : *policy_tokens) {
auto referrer_policy = from_string(token);
if (referrer_policy.has_value() && referrer_policy.value() != ReferrerPolicy::EmptyString)

View file

@ -216,7 +216,7 @@ static void update(JS::VM& vm, GC::Ref<Job> job)
// 1. Append `Service-Worker`/`script` to requests header list.
// Note: See https://w3c.github.io/ServiceWorker/#service-worker
request->header_list()->append(Fetch::Infrastructure::Header::from_string_pair("Service-Worker"sv, "script"sv));
request->header_list()->append(Fetch::Infrastructure::Header::isomorphic_encode("Service-Worker"sv, "script"sv));
// 2. Set requests cache mode to "no-cache" if any of the following are true:
// - registrations update via cache mode is not "all".
@ -266,7 +266,7 @@ static void update(JS::VM& vm, GC::Ref<Job> job)
// 8. Let serviceWorkerAllowed be the result of extracting header list values given `Service-Worker-Allowed` and responses header list.
// Note: See the definition of the Service-Worker-Allowed header in Appendix B: Extended HTTP headers. https://w3c.github.io/ServiceWorker/#service-worker-allowed
auto service_worker_allowed = response->header_list()->extract_header_list_values("Service-Worker-Allowed"sv.bytes());
auto service_worker_allowed = response->header_list()->extract_header_list_values("Service-Worker-Allowed"sv);
// 9. Set policyContainer to the result of creating a policy container from a fetch response given response.
// FIXME: CSP not implemented yet
@ -304,7 +304,7 @@ static void update(JS::VM& vm, GC::Ref<Job> job)
// 14. Else:
else {
// 1. Let maxScope be the result of parsing serviceWorkerAllowed using jobs script url as the base URL.
auto max_scope = DOMURL::parse(service_worker_allowed.get<Vector<ByteBuffer>>()[0], job->script_url);
auto max_scope = DOMURL::parse(service_worker_allowed.get<Vector<ByteString>>()[0], job->script_url);
// 2. If maxScopes origin is jobs script url's origin, then:
if (max_scope->origin().is_same_origin(job->script_url.origin())) {

View file

@ -411,13 +411,10 @@ Optional<StringView> XMLHttpRequest::get_final_encoding() const
}
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-setrequestheader
WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(String const& name_string, String const& value_string)
WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(String const& name, String const& value)
{
auto& realm = this->realm();
auto name = name_string.bytes();
auto value = value_string.bytes();
// 1. If thiss state is not opened, then throw an "InvalidStateError" DOMException.
if (m_state != State::Opened)
return WebIDL::InvalidStateError::create(realm, "XHR readyState is not OPENED"_utf16);
@ -436,7 +433,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(String const& name_
return WebIDL::SyntaxError::create(realm, "Header value contains invalid characters."_utf16);
auto header = Fetch::Infrastructure::Header {
.name = MUST(ByteBuffer::copy(name)),
.name = name.to_byte_string(),
.value = move(normalized_value),
};
@ -451,16 +448,14 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(String const& name_
}
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-open
WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method_string, String const& url)
WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method, String const& url)
{
// 7. If the async argument is omitted, set async to true, and set username and password to null.
return open(method_string, url, true, Optional<String> {}, Optional<String> {});
return open(method, url, true, {}, {});
}
WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method_string, String const& url, bool async, Optional<String> const& username, Optional<String> const& password)
WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method, String const& url, bool async, Optional<String> const& username, Optional<String> const& password)
{
auto method = method_string.bytes();
// 1. If thiss relevant global object is a Window object and its associated Document is not fully active, then throw an "InvalidStateError" DOMException.
if (is<HTML::Window>(HTML::relevant_global_object(*this))) {
auto const& window = static_cast<HTML::Window const&>(HTML::relevant_global_object(*this));
@ -520,7 +515,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method_string, Stri
// Unset thiss upload listener flag.
m_upload_listener = false;
// Set thiss request method to method.
m_request_method = normalized_method.span();
m_request_method = move(normalized_method);
// Set thiss request URL to parsedURL.
m_request_url = parsed_url.release_value();
// Set thiss synchronous flag if async is false; otherwise unset thiss synchronous flag.
@ -568,7 +563,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest
// 4. If body is not null, then:
if (body.has_value()) {
// 1. Let extractedContentType be null.
Optional<ByteBuffer> extracted_content_type;
Optional<ByteString> extracted_content_type;
// 2. If body is a Document, then set thiss request body to body, serialized, converted, and UTF-8 encoded.
if (body->has<GC::Root<DOM::Document>>()) {
@ -589,7 +584,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest
}
// 4. Let originalAuthorContentType be the result of getting `Content-Type` from thiss author request headers.
auto original_author_content_type = m_author_request_headers->get("Content-Type"sv.bytes());
auto original_author_content_type = m_author_request_headers->get("Content-Type"sv);
// 5. If originalAuthorContentType is non-null, then:
if (original_author_content_type.has_value()) {
@ -609,7 +604,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest
auto new_content_type_serialized = content_type_record->serialized();
// 3. Set (`Content-Type`, newContentTypeSerialized) in thiss author request headers.
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, new_content_type_serialized);
auto header = Fetch::Infrastructure::Header::isomorphic_encode("Content-Type"sv, new_content_type_serialized);
m_author_request_headers->set(move(header));
}
}
@ -623,20 +618,18 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest
// NOTE: A document can only be an HTML document or XML document.
// 1. If body is an HTML document, then set (`Content-Type`, `text/html;charset=UTF-8`) in thiss author request headers.
if (document->is_html_document()) {
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html;charset=UTF-8"sv);
m_author_request_headers->set(move(header));
m_author_request_headers->set({ "Content-Type"sv, "text/html;charset=UTF-8"sv });
}
// 2. Otherwise, if body is an XML document, set (`Content-Type`, `application/xml;charset=UTF-8`) in thiss author request headers.
else if (document->is_xml_document()) {
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "application/xml;charset=UTF-8"sv);
m_author_request_headers->set(move(header));
m_author_request_headers->set({ "Content-Type"sv, "application/xml;charset=UTF-8"sv });
} else {
VERIFY_NOT_REACHED();
}
}
// 3. Otherwise, if extractedContentType is not null, set (`Content-Type`, extractedContentType) in thiss author request headers.
else if (extracted_content_type.has_value()) {
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, extracted_content_type.value());
auto header = Fetch::Infrastructure::Header::isomorphic_encode("Content-Type"sv, extracted_content_type.value());
m_author_request_headers->set(move(header));
}
}
@ -650,7 +643,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest
// method
// Thiss request method.
request->set_method(MUST(ByteBuffer::copy(m_request_method.bytes())));
request->set_method(m_request_method);
// URL
// Thiss request URL.
@ -970,7 +963,7 @@ void XMLHttpRequest::set_onreadystatechange(WebIDL::CallbackType* value)
Optional<String> XMLHttpRequest::get_response_header(String const& name) const
{
// The getResponseHeader(name) method steps are to return the result of getting name from thiss responses header list.
auto header_bytes = m_response->header_list()->get(name.bytes());
auto header_bytes = m_response->header_list()->get(name);
if (!header_bytes.has_value())
return {};
@ -979,18 +972,16 @@ Optional<String> XMLHttpRequest::get_response_header(String const& name) const
}
// https://xhr.spec.whatwg.org/#legacy-uppercased-byte-less-than
static ErrorOr<bool> is_legacy_uppercased_byte_less_than(ReadonlyBytes a, ReadonlyBytes b)
static bool is_legacy_uppercased_byte_less_than(ByteString const& a, ByteString const& b)
{
// 1. Let A be a, byte-uppercased.
auto uppercased_a = TRY(ByteBuffer::copy(a));
Infra::byte_uppercase(uppercased_a);
auto uppercased_a = a.to_uppercase();
// 2. Let B be b, byte-uppercased.
auto uppercased_b = TRY(ByteBuffer::copy(b));
Infra::byte_uppercase(uppercased_b);
auto uppercased_b = b.to_uppercase();
// 3. Return A is byte less than B.
return Infra::is_byte_less_than(uppercased_a, uppercased_b);
return Infra::is_byte_less_than(uppercased_a.bytes(), uppercased_b.bytes());
}
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getallresponseheaders
@ -1003,22 +994,15 @@ String XMLHttpRequest::get_all_response_headers() const
auto initial_headers = m_response->header_list()->sort_and_combine();
// 3. Let headers be the result of sorting initialHeaders in ascending order, with a being less than b if as name is legacy-uppercased-byte less than bs name.
// Spec Note: Unfortunately, this is needed for compatibility with deployed content.
// NOTE: quick_sort mutates the collection instead of returning a sorted copy.
// NOTE: Unfortunately, this is needed for compatibility with deployed content.
quick_sort(initial_headers, [](Fetch::Infrastructure::Header const& a, Fetch::Infrastructure::Header const& b) {
return MUST(is_legacy_uppercased_byte_less_than(a.name, b.name));
return is_legacy_uppercased_byte_less_than(a.name, b.name);
});
// 4. For each header in headers, append headers name, followed by a 0x3A 0x20 byte pair, followed by headers value, followed by a 0x0D 0x0A byte pair, to output.
for (auto const& header : initial_headers) {
output.append(header.name);
output.append(0x3A); // ':'
output.append(0x20); // ' '
// FIXME: The spec does not mention isomorphic decode. Spec bug?
auto decoder_header = Infra::isomorphic_decode(header.value);
output.append(decoder_header.bytes());
output.append(0x0D); // '\r'
output.append(0x0A); // '\n'
output.appendff("{}: {}\r\n", header.name, Infra::isomorphic_decode(header.value));
}
// 5. Return output.
@ -1157,10 +1141,8 @@ Fetch::Infrastructure::Status XMLHttpRequest::status() const
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-statustext
WebIDL::ExceptionOr<String> XMLHttpRequest::status_text() const
{
auto& vm = this->vm();
// The statusText getter steps are to return thiss responses status message.
return TRY_OR_THROW_OOM(vm, String::from_utf8(m_response->status_message()));
return MUST(String::from_byte_string(m_response->status_message()));
}
// https://xhr.spec.whatwg.org/#handle-response-end-of-body

View file

@ -58,7 +58,7 @@ public:
WebIDL::ExceptionOr<void> open(String const& method, String const& url, bool async, Optional<String> const& username = Optional<String> {}, Optional<String> const& password = Optional<String> {});
WebIDL::ExceptionOr<void> send(Optional<DocumentOrXMLHttpRequestBodyInit> body);
WebIDL::ExceptionOr<void> set_request_header(String const& header, String const& value);
WebIDL::ExceptionOr<void> set_request_header(String const& name, String const& value);
WebIDL::ExceptionOr<void> set_response_type(Bindings::XMLHttpRequestResponseType);
Optional<String> get_response_header(String const& name) const;