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); GC_DEFINE_ALLOCATOR(Policy);
// https://w3c.github.io/webappsec-csp/#abstract-opdef-parse-a-serialized-csp // 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, // To parse a serialized CSP, given a byte sequence or string serialized, a source source, and a disposition disposition,
// execute the following steps. // 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. // 1. If serialized is a byte sequence, then set serialized to be the result of isomorphic decoding serialized.
auto serialized_string = serialized.has<String>() auto serialized_string = serialized.has<String>()
? serialized.get<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. // 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>(); 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 // 2. For each token returned by extracting header list values given Content-Security-Policy and responses header
// list: // 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) { 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". // 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); 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 // 3. For each token returned by extracting header list values given Content-Security-Policy-Report-Only and
// responses header list: // 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) { 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". // 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); 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; ~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<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&); [[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) Violation::Violation(GC::Ptr<JS::Object> global_object, GC::Ref<Policy const> policy, String directive)
: m_global_object(global_object) : m_global_object(global_object)
, m_policy(policy) , 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 // 1. Let violation be a new violation whose global object is global, policy is policy, effective directive is
// directive, and resource is null. // 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, // 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 // 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 // method
// "POST" // "POST"
request->set_method(MUST(ByteBuffer::copy("POST"sv.bytes()))); request->set_method("POST"sv);
// url // url
// violations 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 // A header list containing a single header whose name is "Content-Type", and value is
// "application/csp-report" // "application/csp-report"
auto header_list = Fetch::Infrastructure::HeaderList::create(vm); 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({ "Content-Type"sv, "application/csp-report"sv });
header_list->append(move(content_type_header));
request->set_header_list(header_list); request->set_header_list(header_list);
// body // body

View file

@ -385,15 +385,15 @@ WebIDL::ExceptionOr<GC::Ref<Document>> Document::create_and_initialize(Type type
document->m_window = window; document->m_window = window;
// NOTE: Non-standard: Pull out the Last-Modified header for use in the lastModified property. // 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. // 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 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); 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. // 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_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_utf8(maybe_http_content_language.value()); !maybe_content_language.is_error()) 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(); 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. // navigationParams's response's service worker timing info.
// 15. If navigationParams's response has a `Refresh` header, then: // 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. // 1. Let value be the isomorphic decoding of the value of the header.
auto value = Infra::isomorphic_decode(maybe_refresh.value()); 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 {}; Optional<u64> length {};
// 9. Let type be null. // 9. Let type be null.
Optional<ByteBuffer> type {}; Optional<ByteString> type {};
// 10. Switch on object. // 10. Switch on object.
TRY(object.visit( TRY(object.visit(
@ -83,7 +83,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
length = blob->size(); length = blob->size();
// If objects type attribute is not the empty byte sequence, set type to its value. // If objects type attribute is not the empty byte sequence, set type to its value.
if (!blob->type().is_empty()) if (!blob->type().is_empty())
type = MUST(ByteBuffer::copy(blob->type().bytes())); type = blob->type().to_byte_string();
return {}; return {};
}, },
[&](ReadonlyBytes bytes) -> WebIDL::ExceptionOr<void> { [&](ReadonlyBytes bytes) -> WebIDL::ExceptionOr<void> {
@ -103,8 +103,7 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
source = serialized_form_data.serialized_data; source = serialized_form_data.serialized_data;
// FIXME: Set length to unclear, see html/6424 for improving this. // 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. // 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 = ByteString::formatted("multipart/form-data; boundary={}", serialized_form_data.boundary);
type = MUST(ByteBuffer::copy(type_string.bytes()));
return {}; return {};
}, },
[&](GC::Root<DOMURL::URLSearchParams> const& url_search_params) -> WebIDL::ExceptionOr<void> { [&](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(); auto search_params_string = url_search_params->to_string();
source = MUST(ByteBuffer::copy(search_params_string.bytes())); source = MUST(ByteBuffer::copy(search_params_string.bytes()));
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`. // 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 {}; return {};
}, },
[&](String const& scalar_value_string) -> WebIDL::ExceptionOr<void> { [&](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. // Set source to the UTF-8 encoding of object.
source = MUST(ByteBuffer::copy(scalar_value_string.bytes())); source = MUST(ByteBuffer::copy(scalar_value_string.bytes()));
// Set type to `text/plain;charset=UTF-8`. // 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 {}; return {};
}, },
[&](GC::Root<Streams::ReadableStream> const& stream) -> WebIDL::ExceptionOr<void> { [&](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)); auto body = Infrastructure::Body::create(vm, *stream, move(source), move(length));
// 14. Return (body, type). // 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) 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. // 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. // 2. If origin is null, then return failure.
// NOTE: Null is not `null`. // NOTE: Null is not `null`.
@ -23,7 +23,7 @@ bool cors_check(Infrastructure::Request const& request, Infrastructure::Response
return false; return false;
// 3. If requests credentials mode is not "include" and origin is `*`, then return success. // 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; return true;
// 4. If the result of byte-serializing a request origin with request is not origin, then return failure. // 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; return true;
// 6. Let credentials be the result of getting `Access-Control-Allow-Credentials` from responses header list. // 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. // 7. If credentials is `true`, then return success.
if (credentials.has_value() && credentials->span() == "true"sv.bytes()) if (credentials == "true"sv)
return true; return true;
// 8. Return failure. // 8. Return failure.
@ -53,7 +53,7 @@ bool tao_check(Infrastructure::Request const& request, Infrastructure::Response
return false; return false;
// 2. Let values be the result of getting, decoding, and splitting `Timing-Allow-Origin` from responses header list. // 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. // 3. If values contains "*", then return success.
if (values.has_value() && values->contains_slow("*"sv)) if (values.has_value() && values->contains_slow("*"sv))

View file

@ -51,6 +51,7 @@
#include <LibWeb/HTML/Window.h> #include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WorkerGlobalScope.h> #include <LibWeb/HTML/WorkerGlobalScope.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h> #include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Loader/LoadRequest.h> #include <LibWeb/Loader/LoadRequest.h>
#include <LibWeb/Loader/ResourceLoader.h> #include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MixedContent/AbstractOperations.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 // - requests client is not null, and requests clients global object is a Window object
&& request.client() && is<HTML::Window>(request.client()->global_object()) && request.client() && is<HTML::Window>(request.client()->global_object())
// - requests method is `GET` // - 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 // - requests unsafe-request flag is not set or requests header list is empty
&& (!request.unsafe_request() || request.header_list()->is_empty())) { && (!request.unsafe_request() || request.header_list()->is_empty())) {
// 1. Assert: requests origin is same origin with requests clients origin. // 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: // 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 `*/*`. // 1. Let value be `*/*`.
auto value = "*/*"sv; auto value = "*/*"sv;
@ -210,17 +211,17 @@ GC::Ref<Infrastructure::FetchController> fetch(JS::Realm& realm, Infrastructure:
} }
// 4. Append (`Accept`, value) to requests header list. // 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)); request.header_list()->append(move(header));
} }
// 12. If requests header list does not contain `Accept-Language`, then user agents should append // 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. // (`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; StringBuilder accept_language;
accept_language.join(","sv, ResourceLoader::the().preferred_languages()); 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)); 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) { if (request->response_tainting() == Infrastructure::Request::ResponseTainting::CORS) {
// 1. Let headerNames be the result of extracting header list values given // 1. Let headerNames be the result of extracting header list values given
// `Access-Control-Expose-Headers` and responses header list. // `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 // 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 // responses CORS-exposed header-name list to all unique header names in responses header
// list. // 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(); auto unique_header_names = response->header_list()->unique_names();
response->set_cors_exposed_header_name_list(move(unique_header_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 if (response->type() == Infrastructure::Response::Type::Opaque
&& internal_response->status() == 206 && internal_response->status() == 206
&& internal_response->range_requested() && 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); 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 // internalResponses status is a null body status, set internalResponses body to null and disregard
// any enqueuing toward it (if any). // any enqueuing toward it (if any).
// NOTE: This standardizes the error handling for servers that violate HTTP. // 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({}); internal_response->set_body({});
// 22. If requests integrity metadata is not the empty string, then: // 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. // The user agent may decide to expose `Server-Timing` headers to non-secure contexts requests as well.
auto client = fetch_params.request()->client(); auto client = fetch_params.request()->client();
if (!response.is_network_error() && client != nullptr && HTML::is_secure_context(*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()) if (server_timing_headers.has_value())
timing_info->set_server_timing_headers(server_timing_headers.release_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. // of fetching.
if (request->current_url().paths().size() == 1 && request->current_url().paths()[0] == "blank"sv) { if (request->current_url().paths().size() == 1 && request->current_url().paths()[0] == "blank"sv) {
auto response = Infrastructure::Response::create(vm); auto response = Infrastructure::Response::create(vm);
response->set_status_message(MUST(ByteBuffer::copy("OK"sv.bytes()))); response->set_status_message("OK"sv);
response->header_list()->append({ "Content-Type"sv, "text/html;charset=utf-8"sv });
auto header = Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html;charset=utf-8"sv);
response->header_list()->append(move(header));
response->set_body(Infrastructure::byte_sequence_as_body(realm, ""sv.bytes())); response->set_body(Infrastructure::byte_sequence_as_body(realm, ""sv.bytes()));
return PendingResponse::create(vm, request, response); 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(); 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] // 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)); 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. // 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(); auto const& type = blob->type();
// 13. If requests header list does not contain `Range`: // 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. // 1. Let bodyWithType be the result of safely extracting blob.
auto body_with_type = safely_extract_body(realm, blob->raw_bytes()); auto body_with_type = safely_extract_body(realm, blob->raw_bytes());
// 2. Set responses status message to `OK`. // 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. // 3. Set responses body to bodyWithTypes body.
response->set_body(body_with_type.body); response->set_body(body_with_type.body);
// 4. Set responses header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ». // 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)); 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)); response->header_list()->append(move(content_type_header));
} }
// 14. Otherwise: // 14. Otherwise:
@ -932,7 +931,7 @@ GC::Ref<PendingResponse> scheme_fetch(JS::Realm& realm, Infrastructure::FetchPar
response->set_range_requested(true); response->set_range_requested(true);
// 2. Let rangeHeader be the result of getting `Range` from requests header list. // 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. // 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); 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); response->set_status(206);
// 14. Set responses status message to `Partial Content`. // 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 « // 15. Set responses header list to «
// (`Content-Length`, serializedSlicedLength), // (`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)); response->header_list()->append(move(content_length_header));
// (`Content-Type`, type), // (`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)); response->header_list()->append(move(content_type_header));
// (`Content-Range`, contentRange) ». // (`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)); 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 // 4. Return a new response whose status message is `OK`, header list is « (`Content-Type`, mimeType) », and
// body is dataURLStructs body as a body. // body is dataURLStructs body as a body.
auto response = Infrastructure::Response::create(vm); 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->header_list()->append(move(header));
response->set_body(Infrastructure::byte_sequence_as_body(realm, data_url_struct.value().body)); 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 // 12. If one of the following is true
if ( if (
// - internalResponses status is 301 or 302 and requests method is `POST` // - 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` // - 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: // then:
) { ) {
// 1. Set requests method to `GET` and requests body to null. // 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({}); request->set_body({});
static constexpr Array request_body_header_names { 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. // 2. For each headerName of request-body-header name, delete headerName from requests header list.
for (auto header_name : request_body_header_names.span()) 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 // 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 "Authorization"sv
}; };
for (auto header_name : cors_non_wildcard_request_header_names) 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 // 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> { class CachePartition : public RefCounted<CachePartition> {
public: public:
// https://httpwg.org/specs/rfc9111.html#constructing.responses.from.caches // 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: // 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()); store_header_and_trailer_fields(response, *cached_response->header_list());
cached_response->set_body(response.body()->clone(realm)); cached_response->set_body(response.body()->clone(realm));
cached_response->set_body_info(response.body_info()); 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->set_status(response.status());
cached_response->url_list().append(http_request.current_url()); cached_response->url_list().append(http_request.current_url());
m_cache.set(http_request.current_url(), move(cached_response)); m_cache.set(http_request.current_url(), move(cached_response));
@ -1570,35 +1569,23 @@ private:
// https://httpwg.org/specs/rfc9111.html#update // https://httpwg.org/specs/rfc9111.html#update
void update_stored_header_fields(Infrastructure::Response const& response, Infrastructure::HeaderList& headers) void update_stored_header_fields(Infrastructure::Response const& response, Infrastructure::HeaderList& headers)
{ {
for (auto& header : *response.header_list()) { for (auto const& header : *response.header_list()) {
auto name = StringView(header.name); if (!is_exempted_for_updating(header.name))
headers.delete_(header.name);
if (is_exempted_for_updating(name))
continue;
headers.delete_(header.name);
} }
for (auto& header : *response.header_list()) { for (auto const& header : *response.header_list()) {
auto name = StringView(header.name); if (!is_exempted_for_updating(header.name))
headers.append(header);
if (is_exempted_for_updating(name))
continue;
headers.append(Infrastructure::Header::copy(header));
} }
} }
// https://httpwg.org/specs/rfc9111.html#storing.fields // https://httpwg.org/specs/rfc9111.html#storing.fields
void store_header_and_trailer_fields(Infrastructure::Response const& response, Web::Fetch::Infrastructure::HeaderList& headers) void store_header_and_trailer_fields(Infrastructure::Response const& response, Web::Fetch::Infrastructure::HeaderList& headers)
{ {
for (auto& header : *response.header_list()) { for (auto const& header : *response.header_list()) {
auto name = StringView(header.name); if (!is_exempted_for_storage(header.name))
headers.append(header);
if (is_exempted_for_storage(name))
continue;
headers.append(Infrastructure::Header::copy(header));
} }
} }
@ -1612,7 +1599,7 @@ private:
return false; return false;
// - the request method is understood by the cache; // - 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; return false;
// - the response status code is final (see Section 15 of [HTTP]); // - 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> {}; : Optional<u64> {};
// 6. Let contentLengthHeaderValue be null. // 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 // 7. If httpRequests body is null and httpRequests method is `POST` or `PUT`, then set
// contentLengthHeaderValue to `0`. // contentLengthHeaderValue to `0`.
if (http_request->body().has<Empty>() && StringView { http_request->method() }.is_one_of("POST"sv, "PUT"sv)) if (http_request->body().has<Empty>() && http_request->method().is_one_of("POST"sv, "PUT"sv))
content_length_header_value = MUST(ByteBuffer::copy("0"sv.bytes())); content_length_header_value = "0"sv;
// 8. If contentLength is non-null, then set contentLengthHeaderValue to contentLength, serialized and // 8. If contentLength is non-null, then set contentLengthHeaderValue to contentLength, serialized and
// isomorphic encoded. // isomorphic encoded.
if (content_length.has_value()) { if (content_length.has_value())
auto content_length_string = String::number(*content_length); content_length_header_value = ByteString::number(*content_length);
content_length_header_value = MUST(ByteBuffer::copy(content_length_string.bytes()));
}
// 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue) to // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue) to
// httpRequests header list. // httpRequests header list.
if (content_length_header_value.has_value()) { if (content_length_header_value.has_value())
auto header = Infrastructure::Header { http_request->header_list()->append({ "Content-Length"sv, content_length_header_value.release_value() });
.name = MUST(ByteBuffer::copy("Content-Length"sv.bytes())),
.value = content_length_header_value.release_value(),
};
http_request->header_list()->append(move(header));
}
// 10. If contentLength is non-null and httpRequests keepalive is true, then: // 10. If contentLength is non-null and httpRequests keepalive is true, then:
if (content_length.has_value() && http_request->keepalive()) { 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: // 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. // 1. Let referrerValue be httpRequests referrer, serialized and isomorphic encoded.
auto referrer_string = http_request->referrer().get<URL::URL>().serialize(); auto referrer_value = Infra::isomorphic_encode(referrer_url->serialize());
auto referrer_value = MUST(ByteBuffer::copy(referrer_string.bytes()));
// 2. Append (`Referer`, referrerValue) to httpRequests header list. // 2. Append (`Referer`, referrerValue) to httpRequests header list.
auto header = Infrastructure::Header { http_request->header_list()->append({ "Referer"sv, move(referrer_value) });
.name = MUST(ByteBuffer::copy("Referer"sv.bytes())),
.value = move(referrer_value),
};
http_request->header_list()->append(move(header));
} }
// 12. Append a request `Origin` header for httpRequest. // 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 // 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. // (`User-Agent`, default `User-Agent` value) to httpRequests header list.
if (!http_request->header_list()->contains("User-Agent"sv.bytes())) { if (!http_request->header_list()->contains("User-Agent"sv))
auto header = Infrastructure::Header { http_request->header_list()->append({ "User-Agent"sv, Infrastructure::default_user_agent_value() });
.name = MUST(ByteBuffer::copy("User-Agent"sv.bytes())),
.value = Infrastructure::default_user_agent_value(),
};
http_request->header_list()->append(move(header));
}
// 16. If httpRequests cache mode is "default" and httpRequests header list contains `If-Modified-Since`, // 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 // `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set httpRequests cache mode to
// "no-store". // "no-store".
if (http_request->cache_mode() == Infrastructure::Request::CacheMode::Default 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-Modified-Since"sv)
|| http_request->header_list()->contains("If-None-Match"sv.bytes()) || http_request->header_list()->contains("If-None-Match"sv)
|| http_request->header_list()->contains("If-Unmodified-Since"sv.bytes()) || http_request->header_list()->contains("If-Unmodified-Since"sv)
|| http_request->header_list()->contains("If-Match"sv.bytes()) || http_request->header_list()->contains("If-Match"sv)
|| http_request->header_list()->contains("If-Range"sv.bytes()))) { || http_request->header_list()->contains("If-Range"sv))) {
http_request->set_cache_mode(Infrastructure::Request::CacheMode::NoStore); 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. // (`Cache-Control`, `max-age=0`) to httpRequests header list.
if (http_request->cache_mode() == Infrastructure::Request::CacheMode::NoCache if (http_request->cache_mode() == Infrastructure::Request::CacheMode::NoCache
&& !http_request->prevent_no_cache_cache_control_header_modification() && !http_request->prevent_no_cache_cache_control_header_modification()
&& !http_request->header_list()->contains("Cache-Control"sv.bytes())) { && !http_request->header_list()->contains("Cache-Control"sv)) {
auto header = Infrastructure::Header::from_string_pair("Cache-Control"sv, "max-age=0"sv); http_request->header_list()->append({ "Cache-Control"sv, "max-age=0"sv });
http_request->header_list()->append(move(header));
} }
// 18. If httpRequests cache mode is "no-store" or "reload", then: // 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) { || http_request->cache_mode() == Infrastructure::Request::CacheMode::Reload) {
// 1. If httpRequests header list does not contain `Pragma`, then append (`Pragma`, `no-cache`) to // 1. If httpRequests header list does not contain `Pragma`, then append (`Pragma`, `no-cache`) to
// httpRequests header list. // httpRequests header list.
if (!http_request->header_list()->contains("Pragma"sv.bytes())) { if (!http_request->header_list()->contains("Pragma"sv))
auto header = Infrastructure::Header::from_string_pair("Pragma"sv, "no-cache"sv); http_request->header_list()->append({ "Pragma"sv, "no-cache"sv });
http_request->header_list()->append(move(header));
}
// 2. If httpRequests header list does not contain `Cache-Control`, then append // 2. If httpRequests header list does not contain `Cache-Control`, then append
// (`Cache-Control`, `no-cache`) to httpRequests header list. // (`Cache-Control`, `no-cache`) to httpRequests header list.
if (!http_request->header_list()->contains("Cache-Control"sv.bytes())) { if (!http_request->header_list()->contains("Cache-Control"sv))
auto header = Infrastructure::Header::from_string_pair("Cache-Control"sv, "no-cache"sv); http_request->header_list()->append({ "Cache-Control"sv, "no-cache"sv });
http_request->header_list()->append(move(header));
}
} }
// 19. If httpRequests header list contains `Range`, then append (`Accept-Encoding`, `identity`) to // 19. If httpRequests header list contains `Range`, then append (`Accept-Encoding`, `identity`) to
// httpRequests header list. // httpRequests header list.
// NOTE: This avoids a failure when handling content codings with a part of an encoded response. // 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. // Additionally, many servers mistakenly ignore `Range` headers if a non-identity encoding is accepted.
if (http_request->header_list()->contains("Range"sv.bytes())) { if (http_request->header_list()->contains("Range"sv))
auto header = Infrastructure::Header::from_string_pair("Accept-Encoding"sv, "identity"sv); http_request->header_list()->append({ "Accept-Encoding"sv, "identity"sv });
http_request->header_list()->append(move(header));
}
// 20. Modify httpRequests header list per HTTP. Do not append a given header if httpRequests header list // 20. Modify httpRequests header list per HTTP. Do not append a given header if httpRequests header list
// contains that headers name. // contains that headers name.
@ -1940,10 +1903,8 @@ GC::Ref<PendingResponse> http_network_or_cache_fetch(JS::Realm& realm, Infrastru
// more details. // more details.
// //
// https://w3c.github.io/gpc/#the-sec-gpc-header-field-for-http-requests // 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())) { if (ResourceLoader::the().enable_global_privacy_control() && !http_request->header_list()->contains("Sec-GPC"sv))
auto header = Infrastructure::Header::from_string_pair("Sec-GPC"sv, "1"sv); http_request->header_list()->append({ "Sec-GPC"sv, "1"sv });
http_request->header_list()->append(move(header));
}
// 21. If includeCredentials is true, then: // 21. If includeCredentials is true, then:
if (include_credentials == IncludeCredentials::Yes) { 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. // 2. If cookies is not the empty string, then append (`Cookie`, cookies) to httpRequests header list.
if (!cookies.is_empty()) { 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)); http_request->header_list()->append(move(header));
} }
} }
// 2. If httpRequests header list does not contain `Authorization`, then: // 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. // 1. Let authorizationValue be null.
auto authorization_value = Optional<String> {}; 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 // 4. If authorizationValue is non-null, then append (`Authorization`, authorizationValue) to
// httpRequests header list. // httpRequests header list.
if (authorization_value.has_value()) { 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)); 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. // if any.
// NOTE: As mandated by HTTP, this still takes the `Vary` header into account. // 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); 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: // 2. If storedResponse is non-null, then:
if (stored_response) { if (stored_response) {
// 1. If cache mode is "default", storedResponse is a stale-while-revalidate 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) { && 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. // 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()) { if (auto etag = stored_response->header_list()->get("ETag"sv); etag.has_value()) {
http_request->header_list()->append(Infrastructure::Header::from_string_pair("If-None-Match"sv, *etag)); 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. // 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()) { if (auto last_modified = stored_response->header_list()->get("Last-Modified"sv); last_modified.has_value()) {
http_request->header_list()->append(Infrastructure::Header::from_string_pair("If-Modified-Since"sv, *last_modified)); 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". // 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; auto forward_response = resolved_forward_response;
// NOTE: TRACE is omitted as it is a forbidden method in Fetch. // 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, // 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 // 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()); response->set_url_list(http_request->url_list());
// 12. If httpRequests header list contains `Range`, then set responses range-requested flag. // 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); response->set_range_requested(true);
// 13. Set responses request-includes-credentials to includeCredentials. // 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>>() && 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 // 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 (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. // 1. Needs testing: multiple `WWW-Authenticate` headers, missing, parsing issues.
// (Red box in the spec, no-op) // (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; LoadRequest load_request;
load_request.set_url(request->current_url()); load_request.set_url(request->current_url());
load_request.set_page(page); 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); load_request.set_store_set_cookie_headers(include_credentials == IncludeCredentials::Yes);
for (auto const& header : *request->header_list()) 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>>()) { if (auto const* body = request->body().get_pointer<GC::Ref<Infrastructure::Body>>()) {
(*body)->source().visit( (*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)); response->set_status(status_code.value_or(200));
if (reason_phrase.has_value()) 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; (void)request;
if constexpr (WEB_FETCH_DEBUG) { 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 {}); log_response(status_code, response_headers, ReadonlyBytes {});
} }
for (auto const& [name, value] : response_headers.headers()) { for (auto const& [name, value] : response_headers.headers())
auto header = Infrastructure::Header::from_latin1_pair(name, value); response->header_list()->append({ name, value });
response->header_list()->append(move(header));
}
// 14. Set responses body to a new body whose stream is stream. // 14. Set responses body to a new body whose stream is stream.
response->set_body(Infrastructure::Body::create(vm, 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, // 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". // referrer policy is requests referrer policy, mode is "cors", and response tainting is "cors".
auto preflight = Fetch::Infrastructure::Request::create(vm); 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_url_list(request.url_list());
preflight->set_initiator(request.initiator()); preflight->set_initiator(request.initiator());
preflight->set_destination(request.destination()); 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); preflight->set_response_tainting(Infrastructure::Request::ResponseTainting::CORS);
// 2. Append (`Accept`, `*/*`) to preflights header list. // 2. Append (`Accept`, `*/*`) to preflights header list.
auto temp_header = Infrastructure::Header::from_string_pair("Accept"sv, "*/*"sv); preflight->header_list()->append({ "Accept"sv, "*/*"sv });
preflight->header_list()->append(move(temp_header));
// 3. Append (`Access-Control-Request-Method`, requests method) to preflights header list. // 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)); preflight->header_list()->append(move(temp_header));
// 4. Let headers be the CORS-unsafe request-header names with requests header list. // 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 `,`. // 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, // NOTE: This intentionally does not use combine, as 0x20 following 0x2C is not the way this was implemented,
// for better or worse. // for better or worse.
ByteBuffer value; auto value = ByteString::join(',', headers);
bool first = true;
for (auto const& header : headers) {
if (!first)
value.append(',');
value.append(header);
first = false;
}
// 2. Append (`Access-Control-Request-Headers`, value) to preflights header list. // 2. Append (`Access-Control-Request-Headers`, value) to preflights header list.
temp_header = Infrastructure::Header { preflight->header_list()->append({ "Access-Control-Request-Headers"sv, move(value) });
.name = MUST(ByteBuffer::copy("Access-Control-Request-Headers"sv.bytes())),
.value = move(value),
};
preflight->header_list()->append(move(temp_header));
} }
// 6. Let response be the result of running HTTP-network-or-cache fetch given a new fetch params whose request is preflight. // 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. // 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())) { 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. // 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 // 2. Let headerNames be the result of extracting header list values given `Access-Control-Allow-Headers` and
// responses header list. // 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. // 3. If either methods or headerNames is failure, return a network error.
if (methods_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>()) { 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)); returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Methods in the CORS-preflight response is syntactically invalid"_string));
return; return;
} }
if (header_names_or_failure.has<Infrastructure::HeaderList::ExtractHeaderParseFailure>()) { 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)); returned_pending_response->resolve(Infrastructure::Response::network_error(vm, "The Access-Control-Allow-Headers in the CORS-preflight response is syntactically invalid"_string));
return; 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. // NOTE: We treat "methods_or_failure" being `Empty` as empty Vector here.
auto methods = methods_or_failure.visit( auto methods = methods_or_failure.visit(
[](Vector<ByteBuffer>& methods) { return move(methods); }, [](Vector<ByteString>& methods) { return move(methods); },
[](auto) -> Vector<ByteBuffer> { return {}; }); [](auto) -> Vector<ByteString> { return {}; });
// NOTE: We treat "header_names_or_failure" being `Empty` as empty Vector here. // NOTE: We treat "header_names_or_failure" being `Empty` as empty Vector here.
auto header_names = header_names_or_failure.visit( auto header_names = header_names_or_failure.visit(
[](Vector<ByteBuffer>& header_names) { return move(header_names); }, [](Vector<ByteString>& header_names) { return move(header_names); },
[](auto) -> Vector<ByteBuffer> { return {}; }); [](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. // 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. // 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()) 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 // 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. // 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 (!methods.contains_slow(request.method()) && !Infrastructure::is_cors_safelisted_method(request.method())) {
if (request.credentials_mode() == Infrastructure::Request::CredentialsMode::Include) { 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; return;
} }
if (!methods.contains_slow("*"sv.bytes())) { 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.", 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 and there was no '*' entry. The header may be missing.", request.method()))));
return; return;
} }
} }
@ -2567,14 +2513,14 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
bool is_in_header_names = false; bool is_in_header_names = false;
for (auto const& allowed_header_name : header_names) { 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; is_in_header_names = true;
break; break;
} }
} }
if (!is_in_header_names) { 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; return;
} }
} }
@ -2588,7 +2534,7 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
bool is_in_header_names = false; bool is_in_header_names = false;
for (auto const& header_name : header_names) { 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; is_in_header_names = true;
break; break;
} }
@ -2596,12 +2542,12 @@ GC::Ref<PendingResponse> cors_preflight_fetch(JS::Realm& realm, Infrastructure::
if (!is_in_header_names) { if (!is_in_header_names) {
if (request.credentials_mode() == Infrastructure::Request::CredentialsMode::Include) { 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; return;
} }
if (!header_names.contains_slow("*"sv.bytes())) { 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.", 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 and there was no '*' entry. The header may be missing.", unsafe_name))));
return; 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. // 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. // 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; auto value = request.destination().has_value()
if (!request.destination().has_value()) { ? Infrastructure::request_destination_to_string(*request.destination())
header_value = MUST(ByteBuffer::copy("empty"sv.bytes())); : "empty"sv;
} else {
header_value = MUST(ByteBuffer::copy(Infrastructure::request_destination_to_string(request.destination().value()).bytes()));
}
// 4. Set a structured field value `Sec-Fetch-Dest`/header in rs header list. // 4. Set a structured field value `Sec-Fetch-Dest`/header in rs header list.
auto header = Infrastructure::Header { request.header_list()->append({ "Sec-Fetch-Dest"sv, value });
.name = MUST(ByteBuffer::copy("Sec-Fetch-Dest"sv.bytes())),
.value = move(header_value),
};
request.header_list()->append(move(header));
} }
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest // 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. // FIXME: This is handled below, as Serenity doesn't have APIs for RFC 8941.
// 3. Set headers value to rs mode. // 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. // 4. Set a structured field value `Sec-Fetch-Mode`/header in rs header list.
auto header = Infrastructure::Header { request.header_list()->append({ "Sec-Fetch-Mode"sv, value });
.name = MUST(ByteBuffer::copy("Sec-Fetch-Mode"sv.bytes())),
.value = move(header_value),
};
request.header_list()->append(move(header));
} }
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-site // 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. // FIXME: This is handled below, as Serenity doesn't have APIs for RFC 8941.
// 3. Set headers value to same-origin. // 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 // 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. // 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: // 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>()); 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. // 1. If url is same origin with rs origin, continue.
if (url.origin().is_same_origin(request_origin)) if (url.origin().is_same_origin(request_origin))
continue; continue;
// 2. Set headers value to cross-site. // 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. // 3. If rs origin is not same site with urls origin, then break.
if (!request_origin.is_same_site(url.origin())) if (!request_origin.is_same_site(url.origin()))
break; break;
// 4. Set headers value to same-site. // 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. // 6. Set a structured field value `Sec-Fetch-Site`/header in rs header list.
auto header = Infrastructure::Header { request.header_list()->append({ "Sec-Fetch-Site"sv, value });
.name = MUST(ByteBuffer::copy("Sec-Fetch-Site"sv.bytes())),
.value = MUST(ByteBuffer::copy(header_value.bytes())),
};
request.header_list()->append(move(header));
} }
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-user // 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. // 4. Set headers value to true.
// NOTE: See https://datatracker.ietf.org/doc/html/rfc8941#name-booleans for boolean format in RFC 8941. // 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. // 5. Set a structured field value `Sec-Fetch-User`/header in rs header list.
auto header = Infrastructure::Header { request.header_list()->append({ "Sec-Fetch-User"sv, value });
.name = MUST(ByteBuffer::copy("Sec-Fetch-User"sv.bytes())),
.value = move(header_value),
};
request.header_list()->append(move(header));
} }
// https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-append-the-fetch-metadata-headers-for-a-request // 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) 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. // 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))); TRY(append(move(header)));
return {}; return {};
} }
// https://fetch.spec.whatwg.org/#dom-headers-delete // 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: // The delete(name) method steps are:
auto name = name_string.bytes();
// 1. If validating (name, ``) for headers returns false, then return. // 1. If validating (name, ``) for headers returns false, then return.
// NOTE: Passing a dummy header value ought not to have any negative repercussions. // 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))) if (!TRY(validate(header)))
return {}; return {};
@ -93,10 +92,9 @@ WebIDL::ExceptionOr<void> Headers::delete_(String const& name_string)
} }
// https://fetch.spec.whatwg.org/#dom-headers-get // 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: // The get(name) method steps are:
auto name = name_string.bytes();
// 1. If name is not a header name, then throw a TypeError. // 1. If name is not a header name, then throw a TypeError.
if (!Infrastructure::is_header_name(name)) if (!Infrastructure::is_header_name(name))
@ -114,23 +112,22 @@ Vector<String> Headers::get_set_cookie()
auto values = Vector<String> {}; auto values = Vector<String> {};
// 1. If thiss header list does not contain `Set-Cookie`, then return « ». // 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; return values;
// 2. Return the values of all headers in thiss header list whose name is a byte-case-insensitive match for // 2. Return the values of all headers in thiss header list whose name is a byte-case-insensitive match for
// `Set-Cookie`, in order. // `Set-Cookie`, in order.
for (auto const& header : *m_header_list) { 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)); values.append(Infra::isomorphic_decode(header.value));
} }
return values; return values;
} }
// https://fetch.spec.whatwg.org/#dom-headers-has // 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: // The has(name) method steps are:
auto name = name_string.bytes();
// 1. If name is not a header name, then throw a TypeError. // 1. If name is not a header name, then throw a TypeError.
if (!Infrastructure::is_header_name(name)) 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 // 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: // The set(name, value) method steps are:
auto name = name_string.bytes();
auto value = value_string.bytes();
// 1. Normalize value. // 1. Normalize value.
auto normalized_value = Infrastructure::normalize_header_value(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. // 2. If validating (name, value) for headers returns false, then return.
if (!TRY(validate(header))) 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. // 2. If temporaryValue is null, then set temporaryValue to value.
if (!temporary_value.has_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. // 3. Otherwise, set temporaryValue to temporaryValue, followed by 0x2C 0x20, followed by value.
else { else {
temporary_value->append(0x2c); temporary_value = ByteString::formatted("{}, {}", *temporary_value, value);
temporary_value->append(0x20);
temporary_value->append(value);
} }
auto temporary_header = Infrastructure::Header { auto temporary_header = Infrastructure::Header {
.name = MUST(ByteBuffer::copy(name)), .name = move(name),
.value = temporary_value.release_value(), .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 }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Array must contain header key/value pair"sv };
// 2. Append (header[0], header[1]) to headers. // 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))); TRY(append(move(header)));
} }
return {}; 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. // 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> { [&](OrderedHashMap<String, String> const& object) -> WebIDL::ExceptionOr<void> {
for (auto const& entry : object) { 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))); TRY(append(move(header)));
} }
return {}; return {};
@ -321,7 +314,7 @@ void Headers::remove_privileged_no_cors_request_headers()
// 1. For each headerName of privileged no-CORS request-header names: // 1. For each headerName of privileged no-CORS request-header names:
for (auto const& header_name : privileged_no_cors_request_header_names) { for (auto const& header_name : privileged_no_cors_request_header_names) {
// 1. Delete headerName from headerss header list. // 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). // A body with type is a tuple that consists of a body (a body) and a type (a header value or null).
struct BodyWithType { struct BodyWithType {
GC::Ref<Body> body; GC::Ref<Body> body;
Optional<ByteBuffer> type; Optional<ByteString> type;
}; };
WEB_API GC::Ref<Body> byte_sequence_as_body(JS::Realm&, ReadonlyBytes); WEB_API GC::Ref<Body> byte_sequence_as_body(JS::Realm&, ReadonlyBytes);

View file

@ -10,16 +10,13 @@
#include <AK/Checked.h> #include <AK/Checked.h>
#include <AK/GenericLexer.h> #include <AK/GenericLexer.h>
#include <AK/QuickSort.h> #include <AK/QuickSort.h>
#include <AK/ScopeGuard.h>
#include <AK/StringUtils.h> #include <AK/StringUtils.h>
#include <LibGC/Heap.h>
#include <LibJS/Runtime/VM.h> #include <LibJS/Runtime/VM.h>
#include <LibRegex/Regex.h> #include <LibRegex/Regex.h>
#include <LibTextCodec/Decoder.h> #include <LibTextCodec/Decoder.h>
#include <LibWeb/Fetch/Infrastructure/HTTP.h> #include <LibWeb/Fetch/Infrastructure/HTTP.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h> #include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h> #include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h>
#include <LibWeb/Infra/ByteSequences.h>
#include <LibWeb/Infra/Strings.h> #include <LibWeb/Infra/Strings.h>
#include <LibWeb/Loader/ResourceLoader.h> #include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MimeSniff/MimeType.h> #include <LibWeb/MimeSniff/MimeType.h>
@ -28,71 +25,38 @@ namespace Web::Fetch::Infrastructure {
GC_DEFINE_ALLOCATOR(HeaderList); GC_DEFINE_ALLOCATOR(HeaderList);
template<typename T> Header Header::isomorphic_encode(StringView name, StringView value)
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)
{ {
return Header { return {
.name = MUST(ByteBuffer::copy(header.name)),
.value = MUST(ByteBuffer::copy(header.value)),
};
}
Header Header::from_string_pair(StringView name, StringView value)
{
return Header {
.name = Infra::isomorphic_encode(name), .name = Infra::isomorphic_encode(name),
.value = Infra::isomorphic_encode(value), .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 // 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: 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. // 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" // 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-Request-Headers"sv,
"Access-Control-Expose-Headers"sv, "Access-Control-Expose-Headers"sv,
"Access-Control-Allow-Headers"sv, "Access-Control-Allow-Headers"sv,
"Access-Control-Allow-Methods"sv) "Access-Control-Allow-Methods"sv)
&& !value.is_empty()) { && !value.is_empty()) {
auto split_values = StringView { value }.split_view(','); Vector<ByteString> trimmed_values;
Vector<ByteBuffer> trimmed_values;
for (auto const& value : split_values) { value.view().for_each_split_view(',', SplitBehavior::Nothing, [&](auto value) {
auto trimmed_value = value.trim(" \t"sv); trimmed_values.append(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));
}
return trimmed_values; return trimmed_values;
} }
// This always ignores the ABNF rules for now and returns the header value as a single list item. // 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) 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 // 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 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 // 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: // 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)) if (!contains(name))
return {}; 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. // 2. Return the values of all headers in list whose name is a byte-case-insensitive match for name, separated from
ByteBuffer buffer; // each other by 0x2C 0x20, in order.
auto first = true; StringBuilder builder;
for (auto const& header : *this) { for (auto const& header : *this) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name)) if (!header.name.equals_ignoring_ascii_case(name))
continue; continue;
if (first) {
first = false; if (!builder.is_empty())
} else { builder.append(", "sv);
buffer.append(0x2c); builder.append(header.value);
buffer.append(0x20);
}
buffer.append(header.value);
} }
return buffer;
return builder.to_byte_string();
} }
// https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split // 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: // 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) void HeaderList::append(Header header)
{ {
// To append a header (name, value) to a header list list, run these steps: // 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. // 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. // NOTE: This reuses the casing of the name of the header already in list, if any. If there are multiple matched
if (contains(name)) { // headers their names will all be identical.
auto matching_header = first_matching([&](auto const& existing_header) { auto matching_header = first_matching([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name); return existing_header.name.equals_ignoring_ascii_case(header.name);
}); });
name.overwrite(0, matching_header->name.data(), matching_header->name.size());
} if (matching_header.has_value())
header.name = matching_header->name;
// 2. Append (name, value) to list. // 2. Append (name, value) to list.
Vector<Header>::append(move(header)); Vector::append(move(header));
} }
// https://fetch.spec.whatwg.org/#concept-header-list-delete // 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) { 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) void HeaderList::set(Header header)
{ {
// To set a header (name, value) in a header list list, run these steps: // 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. // 1. If list contains name, then set the value of the first such header to value and remove the others.
if (contains(name)) { auto it = find_if([&](auto const& existing_header) {
auto matching_index = find_if([&](auto const& existing_header) { return existing_header.name.equals_ignoring_ascii_case(header.name);
return StringView { existing_header.name }.equals_ignoring_ascii_case(name); });
}).index();
auto& matching_header = at(matching_index); if (it != end()) {
matching_header.value = MUST(ByteBuffer::copy(value)); it->value = move(header.value);
size_t i = 0; size_t i = 0;
remove_all_matching([&](auto const& existing_header) { remove_all_matching([&](auto const& existing_header) {
ScopeGuard increment_i = [&]() { i++; }; if (i++ <= it.index())
if (i <= matching_index)
return false; 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. // 2. Otherwise, append header (name, value) to list.
@ -213,18 +175,15 @@ void HeaderList::set(Header header)
void HeaderList::combine(Header header) void HeaderList::combine(Header header)
{ {
// To combine a header (name, value) in a header list list, run these steps: // 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. // 1. If list contains name, then set the value of the first such header to its value, followed by 0x2C 0x20,
if (contains(name)) { // followed by value.
auto matching_header = first_matching([&](auto const& existing_header) { auto matching_header = first_matching([&](auto const& existing_header) {
return StringView { existing_header.name }.equals_ignoring_ascii_case(name); return existing_header.name.equals_ignoring_ascii_case(header.name);
}); });
matching_header->value.append(0x2c);
matching_header->value.append(0x20); if (matching_header.has_value()) {
matching_header->value.append(value); matching_header->value = ByteString::formatted("{}, {}", matching_header->value, header.value);
} }
// 2. Otherwise, append (name, value) to list. // 2. Otherwise, append (name, value) to list.
else { 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. // 1. Let headers be an empty list of headers with the key being the name and value the value.
Vector<Header> headers; 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. // 2. Let names be the result of convert header names to a sorted-lowercase set with all the names of the headers
Vector<ReadonlyBytes> names_list; // in list.
Vector<ByteString> names_list;
names_list.ensure_capacity(size()); names_list.ensure_capacity(size());
for (auto const& header : *this) for (auto const& header : *this)
names_list.unchecked_append(header.name); names_list.unchecked_append(header.name);
auto names = convert_header_names_to_a_sorted_lowercase_set(names_list); auto names = convert_header_names_to_a_sorted_lowercase_set(names_list);
// 3. For each name of names: // 3. For each name of names:
for (auto& name : names) { for (auto& name : names) {
// 1. If name is `set-cookie`, then: // 1. If name is `set-cookie`, then:
if (name == "set-cookie"sv.bytes()) { 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. // 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: // 2. For each value of values:
for (auto const& [header_name, value] : *this) { 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. // 1. Append (name, value) to headers.
auto header = Header::from_string_pair(name, value); headers.append({ name, value });
headers.append(move(header));
} }
} }
} }
@ -270,11 +232,7 @@ Vector<Header> HeaderList::sort_and_combine() const
VERIFY(value.has_value()); VERIFY(value.has_value());
// 3. Append (name, value) to headers. // 3. Append (name, value) to headers.
auto header = Header { headers.empend(move(name), value.release_value());
.name = move(name),
.value = value.release_value(),
};
headers.append(move(header));
} }
} }
@ -283,7 +241,7 @@ Vector<Header> HeaderList::sort_and_combine() const
} }
// https://fetch.spec.whatwg.org/#extract-header-list-values // 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. // 1. If list does not contain name, then return null.
if (!contains(name)) 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. // NOTE: If different error handling is needed, extract the desired header first.
// 3. Let values be an empty list. // 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: // 4. For each header header list contains whose name is name:
for (auto const& header : *this) { for (auto const& header : *this) {
if (!StringView { header.name }.equals_ignoring_ascii_case(name)) if (!header.name.equals_ignoring_ascii_case(name))
continue; continue;
// 1. Let extract be the result of extracting header values from header. // 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 Variant<Empty, u64, HeaderList::ExtractLengthFailure> HeaderList::extract_length() const
{ {
// 1. Let values be the result of getting, decoding, and splitting `Content-Length` from headers. // 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. // 2. If values is null, then return null.
if (!values.has_value()) if (!values.has_value())
@ -363,7 +321,7 @@ Optional<MimeSniff::MimeType> HeaderList::extract_mime_type() const
Optional<MimeSniff::MimeType> mime_type; Optional<MimeSniff::MimeType> mime_type;
// 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers. // 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. // 5. If values is null, then return failure.
if (!values.has_value()) if (!values.has_value())
@ -406,61 +364,65 @@ Optional<MimeSniff::MimeType> HeaderList::extract_mime_type() const
} }
// Non-standard // Non-standard
Vector<ByteBuffer> HeaderList::unique_names() const Vector<ByteString> HeaderList::unique_names() const
{ {
Vector<ByteBuffer> header_names_set; Vector<ByteString> header_names_set;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen; HashTable<StringView, CaseInsensitiveStringTraits> header_names_seen;
for (auto const& header : *this) { for (auto const& header : *this) {
if (header_names_seen.contains(header.name)) if (header_names_seen.contains(header.name))
continue; continue;
header_names_set.append(header.name);
header_names_seen.set(header.name); header_names_seen.set(header.name);
header_names_set.append(MUST(ByteBuffer::copy(header.name)));
} }
return header_names_set; return header_names_set;
} }
// https://fetch.spec.whatwg.org/#header-name // 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. // A header name is a byte sequence that matches the field-name token production.
Regex<ECMA262Parser> regex { R"~~~(^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$)~~~" }; 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 // 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: // A header value is a byte sequence that matches the following conditions:
// - Has no leading or trailing HTTP tab or space bytes. // - Has no leading or trailing HTTP tab or space bytes.
// - Contains no 0x00 (NUL) or HTTP newline bytes. // - Contains no 0x00 (NUL) or HTTP newline bytes.
if (header_value.is_empty()) if (header_value.is_empty())
return true; return true;
auto first_byte = header_value[0]; auto first_byte = header_value[0];
auto last_byte = header_value[header_value.size() - 1]; auto last_byte = header_value[header_value.length() - 1];
if (HTTP_TAB_OR_SPACE_BYTES.span().contains_slow(first_byte) || HTTP_TAB_OR_SPACE_BYTES.span().contains_slow(last_byte))
if (is_http_tab_or_space(first_byte) || is_http_tab_or_space(last_byte))
return false; return false;
return !any_of(header_value, [](auto byte) { 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 // 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()) if (potential_value.is_empty())
return {}; return {};
auto trimmed = StringView { potential_value }.trim(HTTP_WHITESPACE, TrimMode::Both); return potential_value.trim(HTTP_WHITESPACE, TrimMode::Both);
return MUST(ByteBuffer::copy(trimmed.bytes()));
} }
// https://fetch.spec.whatwg.org/#forbidden-header-name // https://fetch.spec.whatwg.org/#forbidden-header-name
bool is_forbidden_request_header(Header const& header) bool is_forbidden_request_header(Header const& header)
{ {
// A header (name, value) is forbidden request-header if these steps return true: // 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: // 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-HTTP-Method-Override"sv,
"X-Method-Override"sv)) { "X-Method-Override"sv)) {
// 1. Let parsedValues be the result of getting, decoding, and splitting value. // 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. // 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. // NB: 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()); })) if (any_of(parsed_values, [](auto const& method) { return is_forbidden_method(method); }))
return true; return true;
} }
@ -519,12 +481,12 @@ bool is_forbidden_request_header(Header const& header)
} }
// https://fetch.spec.whatwg.org/#forbidden-response-header-name // 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: // A forbidden response-header name is a header name that is a byte-case-insensitive match for one of:
// - `Set-Cookie` // - `Set-Cookie`
// - `Set-Cookie2` // - `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-Cookie"sv,
"Set-Cookie2"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 // 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: // 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: // 5. While true:
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. // NOTE: The result might be the empty string.
temporary_value_builder.append(lexer.consume_until(is_any_of("\","sv))); 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 // 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: // 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. // 1. Let headerNamesSet be a new ordered set.
Vector<ByteBuffer> header_names_set; HashTable<StringView, CaseInsensitiveStringTraits> header_names_seen;
HashTable<ReadonlyBytes, CaseInsensitiveBytesTraits<u8 const>> header_names_seen; Vector<ByteString> header_names_set;
// 2. For each name of headerNames, append the result of byte-lowercasing name to headerNamesSet. // 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)) if (header_names_seen.contains(name))
continue; continue;
auto bytes = MUST(ByteBuffer::copy(name));
Infra::byte_lowercase(bytes);
header_names_seen.set(name); 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. // 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) { quick_sort(header_names_set);
return StringView { a } < StringView { b }; return header_names_set;
});
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;
} }
// https://fetch.spec.whatwg.org/#build-a-content-range // 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 // 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. // 1. Let data be the isomorphic decoding of value.
auto const data = Infra::isomorphic_decode(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) 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: // To determine whether a header (name, value) is a CORS-safelisted request-header, run these steps:
auto const& [name, value] = header;
auto const& value = header.value;
// 1. If values length is greater than 128, then return false. // 1. If values length is greater than 128, then return false.
if (value.size() > 128) if (value.length() > 128)
return false; return false;
// 2. Byte-lowercase name and switch on the result: // 2. Byte-lowercase name and switch on the result:
auto name = StringView { header.name };
// `accept` // `accept`
if (name.equals_ignoring_ascii_case("accept"sv)) { if (name.equals_ignoring_ascii_case("accept"sv)) {
// If value contains a CORS-unsafe request-header byte, then return false. // 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; return false;
} }
// `accept-language` // `accept-language`
// `content-language` // `content-language`
else if (name.is_one_of_ignoring_ascii_case("accept-language"sv, "content-language"sv)) { 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 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) { if (any_of(value, [](auto byte) {
return !(is_ascii_digit(byte) || is_ascii_alpha(byte) || " *,-.;="sv.contains(static_cast<char>(byte))); return !(is_ascii_digit(byte) || is_ascii_alpha(byte) || " *,-.;="sv.contains(byte));
})) }))
return false; return false;
} }
// `content-type` // `content-type`
else if (name.equals_ignoring_ascii_case("content-type"sv)) { else if (name.equals_ignoring_ascii_case("content-type"sv)) {
// 1. If value contains a CORS-unsafe request-header byte, then return false. // 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; return false;
// 2. Let mimeType be the result of parsing the result of isomorphic decoding value. // 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 // 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: // The CORS-unsafe request-header names, given a header list headers, are determined as follows:
// 1. Let unsafeNames be a new list. // 1. Let unsafeNames be a new list.
Vector<ReadonlyBytes> unsafe_names; Vector<ByteString> unsafe_names;
// 2. Let potentiallyUnsafeNames be a new list. // 2. Let potentiallyUnsafeNames be a new list.
Vector<ReadonlyBytes> potentially_unsafe_names; Vector<ByteString> potentially_unsafe_names;
// 3. Let safelistValueSize be 0. // 3. Let safelistValueSize be 0.
Checked<size_t> safelist_value_size = 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) { for (auto const& header : headers) {
// 1. If header is not a CORS-safelisted request-header, then append headers name to unsafeNames. // 1. If header is not a CORS-safelisted request-header, then append headers name to unsafeNames.
if (!is_cors_safelisted_request_header(header)) { 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 { else {
potentially_unsafe_names.append(header.name.span()); potentially_unsafe_names.append(header.name);
safelist_value_size += header.value.size(); safelist_value_size += header.value.length();
} }
} }
// 5. If safelistValueSize is greater than 1024, then for each name of potentiallyUnsafeNames, append name to unsafeNames. // 5. If safelistValueSize is greater than 1024, then for each name of potentiallyUnsafeNames, append name to
if (safelist_value_size.has_overflow() || safelist_value_size.value() > 1024) { // unsafeNames.
for (auto const& name : potentially_unsafe_names) if (safelist_value_size.has_overflow() || safelist_value_size.value() > 1024)
unsafe_names.append(name); unsafe_names.extend(move(potentially_unsafe_names));
}
// 6. Return the result of convert header names to a sorted-lowercase set with unsafeNames. // 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 // 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`. // 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 // 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 // A privileged no-CORS request-header name is a header name that is a byte-case-insensitive match for one of
// - `Range`. // - `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 // 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 // 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` // - `Cache-Control`
@ -863,7 +815,7 @@ bool is_cors_safelisted_response_header_name(ReadonlyBytes header_name, Span<Rea
// - `Last-Modified` // - `Last-Modified`
// - `Pragma` // - `Pragma`
// - Any item in list that is not a forbidden response-header name. // - 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, "Cache-Control"sv,
"Content-Language"sv, "Content-Language"sv,
"Content-Length"sv, "Content-Length"sv,
@ -872,20 +824,20 @@ bool is_cors_safelisted_response_header_name(ReadonlyBytes header_name, Span<Rea
"Last-Modified"sv, "Last-Modified"sv,
"Pragma"sv) "Pragma"sv)
|| any_of(list, [&](auto list_header_name) { || 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); && !is_forbidden_response_header_name(list_header_name);
}); });
} }
// https://fetch.spec.whatwg.org/#no-cors-safelisted-request-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 // A no-CORS-safelisted request-header name is a header name that is a byte-case-insensitive match for one of
// - `Accept` // - `Accept`
// - `Accept-Language` // - `Accept-Language`
// - `Content-Language` // - `Content-Language`
// - `Content-Type` // - `Content-Type`
return StringView { header_name }.is_one_of_ignoring_ascii_case( return header_name.is_one_of_ignoring_ascii_case(
"Accept"sv, "Accept"sv,
"Accept-Language"sv, "Accept-Language"sv,
"Content-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 // 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. // 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 #pragma once
#include <AK/ByteBuffer.h> #include <AK/ByteString.h>
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/HashTable.h>
#include <AK/Optional.h> #include <AK/Optional.h>
#include <AK/String.h> #include <AK/String.h>
#include <AK/Vector.h> #include <AK/Vector.h>
@ -25,14 +22,12 @@ namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-header // 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). // A header is a tuple that consists of a name (a header name) and value (a header value).
struct WEB_API Header { struct WEB_API Header {
[[nodiscard]] static Header copy(Header const&); [[nodiscard]] static Header isomorphic_encode(StringView, StringView);
[[nodiscard]] static Header from_string_pair(StringView, StringView);
[[nodiscard]] static Header from_latin1_pair(StringView, StringView);
[[nodiscard]] Optional<Vector<ByteBuffer>> extract_header_values() const; Optional<Vector<ByteString>> extract_header_values() const;
ByteBuffer name; ByteString name;
ByteBuffer value; ByteString value;
}; };
// https://fetch.spec.whatwg.org/#concept-header-list // https://fetch.spec.whatwg.org/#concept-header-list
@ -51,24 +46,24 @@ public:
using Vector::end; using Vector::end;
using Vector::is_empty; using Vector::is_empty;
[[nodiscard]] bool contains(ReadonlyBytes) const; [[nodiscard]] bool contains(StringView) const;
[[nodiscard]] Optional<ByteBuffer> get(ReadonlyBytes) const; [[nodiscard]] Optional<ByteString> get(StringView) const;
[[nodiscard]] Optional<Vector<String>> get_decode_and_split(ReadonlyBytes) const; [[nodiscard]] Optional<Vector<String>> get_decode_and_split(StringView) const;
void append(Header); void append(Header);
void delete_(ReadonlyBytes name); void delete_(StringView name);
void set(Header); void set(Header);
void combine(Header); void combine(Header);
[[nodiscard]] Vector<Header> sort_and_combine() const; [[nodiscard]] Vector<Header> sort_and_combine() const;
struct ExtractHeaderParseFailure { }; 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 { }; struct ExtractLengthFailure { };
[[nodiscard]] Variant<Empty, u64, ExtractLengthFailure> extract_length() const; [[nodiscard]] Variant<Empty, u64, ExtractLengthFailure> extract_length() const;
[[nodiscard]] Optional<MimeSniff::MimeType> extract_mime_type() const; [[nodiscard]] Optional<MimeSniff::MimeType> extract_mime_type() const;
[[nodiscard]] Vector<ByteBuffer> unique_names() const; [[nodiscard]] Vector<ByteString> unique_names() const;
}; };
struct RangeHeaderValue { struct RangeHeaderValue {
@ -76,29 +71,29 @@ struct RangeHeaderValue {
Optional<u64> end; Optional<u64> end;
}; };
[[nodiscard]] bool is_header_name(ReadonlyBytes); [[nodiscard]] bool is_header_name(StringView);
[[nodiscard]] bool is_header_value(ReadonlyBytes); [[nodiscard]] bool is_header_value(StringView);
[[nodiscard]] ByteBuffer normalize_header_value(ReadonlyBytes); [[nodiscard]] ByteString normalize_header_value(StringView);
[[nodiscard]] bool is_forbidden_request_header(Header const&); [[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]] 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]] Vector<String> get_decode_and_split_header_value(StringView);
[[nodiscard]] OrderedHashTable<ByteBuffer> convert_header_names_to_a_sorted_lowercase_set(Span<ReadonlyBytes>); [[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 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_safelisted_request_header(Header const&);
[[nodiscard]] bool is_cors_unsafe_request_header_byte(u8); [[nodiscard]] bool is_cors_unsafe_request_header_byte(u8);
[[nodiscard]] WEB_API OrderedHashTable<ByteBuffer> get_cors_unsafe_header_names(HeaderList const&); [[nodiscard]] WEB_API Vector<ByteString> get_cors_unsafe_header_names(HeaderList const&);
[[nodiscard]] WEB_API bool is_cors_non_wildcard_request_header_name(ReadonlyBytes); [[nodiscard]] WEB_API bool is_cors_non_wildcard_request_header_name(StringView);
[[nodiscard]] bool is_privileged_no_cors_request_header_name(ReadonlyBytes); [[nodiscard]] bool is_privileged_no_cors_request_header_name(StringView);
[[nodiscard]] bool is_cors_safelisted_response_header_name(ReadonlyBytes, Span<ReadonlyBytes>); [[nodiscard]] bool is_cors_safelisted_response_header_name(StringView, ReadonlySpan<StringView>);
[[nodiscard]] bool is_no_cors_safelisted_request_header_name(ReadonlyBytes); [[nodiscard]] bool is_no_cors_safelisted_request_header_name(StringView);
[[nodiscard]] bool is_no_cors_safelisted_request_header(Header const&); [[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 * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <AK/ByteBuffer.h>
#include <AK/StringView.h>
#include <LibRegex/Regex.h> #include <LibRegex/Regex.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h> #include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h>
#include <LibWeb/Infra/ByteSequences.h>
namespace Web::Fetch::Infrastructure { namespace Web::Fetch::Infrastructure {
// https://fetch.spec.whatwg.org/#concept-method // 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. // A method is a byte sequence that matches the method token production.
Regex<ECMA262Parser> regex { R"~~~(^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$)~~~" }; 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 // 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`. // 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 // 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`. // 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 // 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. // To normalize a method, if it is a byte-case-insensitive match for `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`,
auto bytes = MUST(ByteBuffer::copy(method)); // or `PUT`, byte-uppercase it.
if (StringView { method }.is_one_of_ignoring_ascii_case("DELETE"sv, "GET"sv, "HEAD"sv, "OPTIONS"sv, "POST"sv, "PUT"sv)) static auto NORMALIZED_METHODS = to_array<ByteString>({ "DELETE"sv, "GET"sv, "HEAD"sv, "OPTIONS"sv, "POST"sv, "PUT"sv });
Infra::byte_uppercase(bytes);
return bytes; 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 #pragma once
#include <AK/Forward.h> #include <AK/ByteString.h>
#include <AK/StringView.h>
#include <LibWeb/Export.h> #include <LibWeb/Export.h>
namespace Web::Fetch::Infrastructure { namespace Web::Fetch::Infrastructure {
[[nodiscard]] bool is_method(ReadonlyBytes); [[nodiscard]] bool is_method(StringView);
[[nodiscard]] WEB_API bool is_cors_safelisted_method(ReadonlyBytes); [[nodiscard]] WEB_API bool is_cors_safelisted_method(StringView);
[[nodiscard]] bool is_forbidden_method(ReadonlyBytes); [[nodiscard]] bool is_forbidden_method(StringView);
[[nodiscard]] ByteBuffer normalize_method(ReadonlyBytes); [[nodiscard]] ByteString normalize_method(StringView);
} }

View file

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

View file

@ -8,6 +8,7 @@
#pragma once #pragma once
#include <AK/ByteBuffer.h> #include <AK/ByteBuffer.h>
#include <AK/ByteString.h>
#include <AK/Error.h> #include <AK/Error.h>
#include <AK/Forward.h> #include <AK/Forward.h>
#include <AK/Optional.h> #include <AK/Optional.h>
@ -171,8 +172,8 @@ public:
[[nodiscard]] static GC::Ref<Request> create(JS::VM&); [[nodiscard]] static GC::Ref<Request> create(JS::VM&);
[[nodiscard]] ReadonlyBytes method() const LIFETIME_BOUND { return m_method; } [[nodiscard]] ByteString const& method() const { return m_method; }
void set_method(ByteBuffer method) { m_method = move(method); } void set_method(ByteString method) { m_method = move(method); }
[[nodiscard]] bool local_urls_only() const { return m_local_urls_only; } [[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; } 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]] RedirectTaint redirect_taint() const;
[[nodiscard]] String serialize_origin() 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; [[nodiscard]] GC::Ref<Request> clone(JS::Realm&) const;
@ -335,7 +336,7 @@ private:
// https://fetch.spec.whatwg.org/#concept-request-method // https://fetch.spec.whatwg.org/#concept-request-method
// A request has an associated method (a method). Unless stated otherwise it is `GET`. // 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 // https://fetch.spec.whatwg.org/#local-urls-only-flag
// A request has an associated local-URLs-only flag. Unless stated otherwise it is unset. // 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> {}; return Optional<URL::URL> {};
// 2. Let location be the result of extracting header list values given `Location` and responses header list. // 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 location_values_or_failure = m_header_list->extract_header_list_values("Location"sv);
auto const* location_values = location_values_or_failure.get_pointer<Vector<ByteBuffer>>(); auto const* location_values = location_values_or_failure.get_pointer<Vector<ByteString>>();
if (!location_values || location_values->size() != 1) if (!location_values || location_values->size() != 1)
return OptionalNone {}; 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. // 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; Optional<AK::Duration> age;
if (auto const age_header = header_list()->get("Age"sv.bytes()); age_header.has_value()) { if (auto const age_header = header_list()->get("Age"sv); age_header.has_value()) {
if (auto converted_age = StringView { *age_header }.to_number<u64>(); converted_age.has_value()) if (auto converted_age = age_header->to_number<u64>(); converted_age.has_value())
age = AK::Duration::from_seconds(converted_age.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 // https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
u64 Response::freshness_lifetime() const 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()) if (!elem.has_value())
return 0; 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 // https://httpwg.org/specs/rfc5861.html#n-the-stale-while-revalidate-cache-control-extension
u64 Response::stale_while_revalidate_lifetime() const 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()) if (!elem.has_value())
return 0; 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 // 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 // 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. // 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()) 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); auto header_list = HeaderList::create(vm);
for (auto const& header : *internal_response->header_list()) { for (auto const& header : *internal_response->header_list()) {

View file

@ -6,7 +6,7 @@
#pragma once #pragma once
#include <AK/ByteBuffer.h> #include <AK/ByteString.h>
#include <AK/Error.h> #include <AK/Error.h>
#include <AK/Forward.h> #include <AK/Forward.h>
#include <AK/Optional.h> #include <AK/Optional.h>
@ -78,8 +78,8 @@ public:
[[nodiscard]] virtual Status status() const { return m_status; } [[nodiscard]] virtual Status status() const { return m_status; }
virtual void set_status(Status status) { m_status = status; } virtual void set_status(Status status) { m_status = status; }
[[nodiscard]] virtual ReadonlyBytes status_message() const LIFETIME_BOUND { return m_status_message; } [[nodiscard]] virtual ByteString const& status_message() const { return m_status_message; }
virtual void set_status_message(ByteBuffer status_message) { m_status_message = move(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; } [[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; } 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; } [[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); } 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; } [[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<ByteBuffer> cors_exposed_header_name_list) { m_cors_exposed_header_name_list = move(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; } [[nodiscard]] virtual bool range_requested() const { return m_range_requested; }
virtual void set_range_requested(bool range_requested) { m_range_requested = 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; } 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; } [[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; } [[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_aborted_network_error() const;
[[nodiscard]] bool is_network_error() const; [[nodiscard]] bool is_network_error() const;
@ -153,7 +153,7 @@ private:
// https://fetch.spec.whatwg.org/#concept-response-status-message // 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. // 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 // 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. // 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 // 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. // 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 // https://fetch.spec.whatwg.org/#concept-response-range-requested-flag
// A response has an associated range-requested flag, which is initially unset. // A response has an associated range-requested flag, which is initially unset.
@ -200,14 +200,14 @@ private:
u64 stale_while_revalidate_lifetime() const; u64 stale_while_revalidate_lifetime() const;
// Non-standard // Non-standard
ByteBuffer m_method; ByteString m_method;
MonotonicTime m_response_time; MonotonicTime m_response_time;
Optional<String> m_network_error_message; Optional<String> m_network_error_message;
public: public:
[[nodiscard]] ByteBuffer const& method() const { return m_method; } [[nodiscard]] ByteString const& method() const { return m_method; }
void set_method(ByteBuffer method) { m_method = move(method); } void set_method(ByteString method) { m_method = move(method); }
}; };
// https://fetch.spec.whatwg.org/#concept-filtered-response // https://fetch.spec.whatwg.org/#concept-filtered-response
@ -230,8 +230,8 @@ public:
[[nodiscard]] virtual Status status() const override { return m_internal_response->status(); } [[nodiscard]] virtual Status status() const override { return m_internal_response->status(); }
virtual void set_status(Status status) override { m_internal_response->set_status(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(); } [[nodiscard]] virtual ByteString const& status_message() const 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)); } 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(); } [[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); } 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(); } [[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)); } 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(); } [[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<ByteBuffer> cors_exposed_header_name_list) override { m_internal_response->set_cors_exposed_header_name_list(move(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(); } [[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); } 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> const& url_list() const override { return m_url_list; }
[[nodiscard]] virtual Vector<URL::URL>& url_list() 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 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::Ref<HeaderList> header_list() const override { return m_header_list; }
[[nodiscard]] virtual GC::Ptr<Body> body() const override { return nullptr; } [[nodiscard]] virtual GC::Ptr<Body> body() const override { return nullptr; }
@ -327,6 +327,7 @@ private:
virtual void visit_edges(JS::Cell::Visitor&) override; virtual void visit_edges(JS::Cell::Visitor&) override;
Vector<URL::URL> m_url_list; Vector<URL::URL> m_url_list;
ByteString const m_method;
GC::Ref<HeaderList> m_header_list; GC::Ref<HeaderList> m_header_list;
}; };
@ -340,7 +341,7 @@ public:
[[nodiscard]] virtual Type type() const override { return Type::OpaqueRedirect; } [[nodiscard]] virtual Type type() const override { return Type::OpaqueRedirect; }
[[nodiscard]] virtual Status status() const override { return 0; } [[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::Ref<HeaderList> header_list() const override { return m_header_list; }
[[nodiscard]] virtual GC::Ptr<Body> body() const override { return nullptr; } [[nodiscard]] virtual GC::Ptr<Body> body() const override { return nullptr; }
@ -349,6 +350,7 @@ private:
virtual void visit_edges(JS::Cell::Visitor&) override; virtual void visit_edges(JS::Cell::Visitor&) override;
ByteString const m_method;
GC::Ref<HeaderList> m_header_list; GC::Ref<HeaderList> m_header_list;
}; };

View file

@ -183,7 +183,7 @@ WebIDL::ExceptionOr<GC::Ref<Request>> Request::construct_impl(JS::Realm& realm,
// method // method
// requests method. // requests method.
request->set_method(MUST(ByteBuffer::copy(input_request->method()))); request->set_method(input_request->method());
// header list // header list
// A copy of requests 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; auto method = *init.method;
// 2. If method is not a method or method is a forbidden method, then throw a TypeError. // 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 }; 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 }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Method must not be one of CONNECT, TRACE, or TRACK"sv };
// 3. Normalize method. // 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. // 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. // 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. // 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>>()) { if (auto* header_list = headers.get_pointer<GC::Ref<Infrastructure::HeaderList>>()) {
for (auto& header : *header_list->ptr()) 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. // 5. Otherwise, fill thiss headers with headers.
else { 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(); 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. // 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 }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Method must not be GET or HEAD when body is provided"sv };
// 36. Let initBody be null. // 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; 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. // 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())) if (type.has_value() && !request_object->headers()->header_list()->contains("Content-Type"sv))
TRY(request_object->headers()->append(Infrastructure::Header::from_string_pair("Content-Type"sv, type->span()))); 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. // 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 String Request::method() const
{ {
// The method getter steps are to return thiss requests method. // 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 // 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); m_response->set_status(init.status);
// 4. Set responses responses status message to init["statusText"]. // 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"]. // 5. If init["headers"] exists, then fill responses headers with init["headers"].
if (init.headers.has_value()) if (init.headers.has_value())
@ -127,13 +127,8 @@ WebIDL::ExceptionOr<void> Response::initialize_response(ResponseInit const& init
m_response->set_body(body->body); 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. // 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())) { if (body->type.has_value() && !m_response->header_list()->contains("Content-Type"sv))
auto header = Infrastructure::Header { m_response->header_list()->append({ "Content-Type"sv, *body->type });
.name = MUST(ByteBuffer::copy("Content-Type"sv.bytes())),
.value = MUST(ByteBuffer::copy(body->type->span())),
};
m_response->header_list()->append(move(header));
}
} }
return {}; return {};
@ -204,7 +199,7 @@ WebIDL::ExceptionOr<GC::Ref<Response>> Response::redirect(JS::VM& vm, String con
auto value = parsed_url->serialize(); auto value = parsed_url->serialize();
// 7. Append (`Location`, value) to responseObjects responses header list. // 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)); response_object->response()->header_list()->append(move(header));
// 8. Return responseObject. // 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"). // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
auto body_with_type = Infrastructure::BodyWithType { auto body_with_type = Infrastructure::BodyWithType {
.body = body, .body = body,
.type = MUST(ByteBuffer::copy("application/json"sv.bytes())) .type = "application/json"sv,
}; };
TRY(response_object->initialize_response(init, move(body_with_type))); TRY(response_object->initialize_response(init, move(body_with_type)));
@ -278,7 +273,7 @@ bool Response::ok() const
String Response::status_text() const String Response::status_text() const
{ {
// The statusText getter steps are to return thiss responses status message. // 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 // 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); request->set_client(&settings);
// 10. User agents may set (`Accept`, `text/event-stream`) in request's header list. // 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({ "Accept"sv, "text/event-stream"sv });
request->header_list()->set(move(header));
// 11. Set request's cache mode to "no-store". // 11. Set request's cache mode to "no-store".
request->set_cache_mode(Fetch::Infrastructure::Request::CacheMode::NoStore); 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()) { if (!m_last_event_id.is_empty()) {
// 1. Let lastEventIDValue be the EventSource object's last event ID string, encoded as UTF-8. // 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. // 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); auto header = Fetch::Infrastructure::Header::isomorphic_encode("Last-Event-ID"sv, m_last_event_id);
request->header_list()->set(header); 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. // 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 // body: the UTF-8 encoding of documentResource, as a body
auto response = Fetch::Infrastructure::Response::create(vm); auto response = Fetch::Infrastructure::Response::create(vm);
response->url_list().append(URL::about_srcdoc()); response->url_list().append(URL::about_srcdoc());
response->header_list()->append({ "Content-Type"sv, "text/html"sv });
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html"sv);
response->header_list()->append(move(header));
response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, document_resource.get<String>().bytes())); 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. // 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: // 6. If documentResource is a POST resource:
if (auto* post_resource = document_resource.get_pointer<POSTResource>()) { if (auto* post_resource = document_resource.get_pointer<POSTResource>()) {
// 1. Set request's method to `POST`. // 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. // 2. Set request's body to documentResource's request body.
request->set_body(document_resource.get<POSTResource>().request_body.value()); 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(); 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)); 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 // body: the UTF-8 encoding of result, as a body
auto response = Fetch::Infrastructure::Response::create(vm); auto response = Fetch::Infrastructure::Response::create(vm);
response->url_list().append(active_document()->url()); response->url_list().append(active_document()->url());
response->header_list()->append({ "Content-Type"sv, "text/html"sv });
auto header = Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html"sv);
response->header_list()->append(move(header));
response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, result.bytes())); response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, result.bytes()));
// 12. Let policyContainer be targetNavigable's active document's policy container. // 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; 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". // 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)) if (Fetch::Infrastructure::is_cors_safelisted_request_header(content_type_header))
cors_mode = Fetch::Infrastructure::Request::Mode::NoCORS; cors_mode = Fetch::Infrastructure::Request::Mode::NoCORS;
// Append a Content-Type header with value contentType to headerList. // 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: // 7.1 Let req be a new request, initialized as follows:
auto req = Fetch::Infrastructure::Request::create(vm); auto req = Fetch::Infrastructure::Request::create(vm);
req->set_method(MUST(ByteBuffer::copy("POST"sv.bytes()))); // method: POST req->set_method("POST"sv); // method: POST
req->set_client(&relevant_settings_object); // client: this's relevant settings object req->set_client(&relevant_settings_object); // client: this's relevant settings object
req->set_url_list({ parsed_url.release_value() }); // url: parsedUrl req->set_url_list({ parsed_url.release_value() }); // url: parsedUrl
req->set_header_list(header_list); // header list: headerList req->set_header_list(header_list); // header list: headerList
req->set_origin(origin); // origin: origin req->set_origin(origin); // origin: origin
req->set_keepalive(true); // keepalive: true req->set_keepalive(true); // keepalive: true
if (transmitted_data) if (transmitted_data)
req->set_body(GC::Ref<Fetch::Infrastructure::Body> { *transmitted_data }); // body: transmittedData req->set_body(GC::Ref<Fetch::Infrastructure::Body> { *transmitted_data }); // body: transmittedData
req->set_mode(cors_mode); // mode: corsMode 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 // 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 // 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. // 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". // NB: This is essentially spec-speak for "Encode as ISO-8859-1 / Latin-1".
ByteBuffer buf = {}; StringBuilder builder(input.length());
for (auto code_point : Utf8View { input }) { for (auto code_point : Utf8View { input }) {
// VERIFY(code_point <= 0xFF); // VERIFY(code_point <= 0xFF);
if (code_point > 0xFF) if (code_point > 0xFF)
dbgln("FIXME: Trying to isomorphic encode a string with code points > U+00FF."); 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 // 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 isomorphic decode a byte sequence input, return a string whose code point length is equal to inputs length
// to inputs length and whose code points have the same values as the values of inputs bytes, in the same order. // 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". // NB: This is essentially spec-speak for "Decode as ISO-8859-1 / Latin-1".
StringBuilder builder(input.size()); StringBuilder builder(input.length());
for (u8 code_point : input) {
builder.append_code_point(code_point); for (auto byte : input.bytes())
} builder.append_code_point(byte);
return builder.to_string_without_validation(); 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); Utf16String strip_and_collapse_whitespace(Utf16String const& string);
WEB_API bool is_code_unit_prefix(StringView potential_prefix, StringView input); WEB_API bool is_code_unit_prefix(StringView potential_prefix, StringView input);
WEB_API ErrorOr<String> convert_to_scalar_value_string(StringView string); WEB_API ErrorOr<String> convert_to_scalar_value_string(StringView string);
ByteBuffer isomorphic_encode(StringView input); ByteString isomorphic_encode(StringView input);
WEB_API String isomorphic_decode(ReadonlyBytes input); WEB_API String isomorphic_decode(StringView input);
bool code_unit_less_than(StringView a, StringView b); 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) 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. // 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. // 2. Let policy be the empty string.
auto policy = ReferrerPolicy::EmptyString; 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. // 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) { for (auto const& token : *policy_tokens) {
auto referrer_policy = from_string(token); auto referrer_policy = from_string(token);
if (referrer_policy.has_value() && referrer_policy.value() != ReferrerPolicy::EmptyString) 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. // 1. Append `Service-Worker`/`script` to requests header list.
// Note: See https://w3c.github.io/ServiceWorker/#service-worker // 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: // 2. Set requests cache mode to "no-cache" if any of the following are true:
// - registrations update via cache mode is not "all". // - 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. // 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 // 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. // 9. Set policyContainer to the result of creating a policy container from a fetch response given response.
// FIXME: CSP not implemented yet // FIXME: CSP not implemented yet
@ -304,7 +304,7 @@ static void update(JS::VM& vm, GC::Ref<Job> job)
// 14. Else: // 14. Else:
else { else {
// 1. Let maxScope be the result of parsing serviceWorkerAllowed using jobs script url as the base URL. // 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: // 2. If maxScopes origin is jobs script url's origin, then:
if (max_scope->origin().is_same_origin(job->script_url.origin())) { 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 // 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& 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. // 1. If thiss state is not opened, then throw an "InvalidStateError" DOMException.
if (m_state != State::Opened) if (m_state != State::Opened)
return WebIDL::InvalidStateError::create(realm, "XHR readyState is not OPENED"_utf16); 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); return WebIDL::SyntaxError::create(realm, "Header value contains invalid characters."_utf16);
auto header = Fetch::Infrastructure::Header { auto header = Fetch::Infrastructure::Header {
.name = MUST(ByteBuffer::copy(name)), .name = name.to_byte_string(),
.value = move(normalized_value), .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 // 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. // 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. // 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))) { if (is<HTML::Window>(HTML::relevant_global_object(*this))) {
auto const& window = static_cast<HTML::Window const&>(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. // Unset thiss upload listener flag.
m_upload_listener = false; m_upload_listener = false;
// Set thiss request method to method. // Set thiss request method to method.
m_request_method = normalized_method.span(); m_request_method = move(normalized_method);
// Set thiss request URL to parsedURL. // Set thiss request URL to parsedURL.
m_request_url = parsed_url.release_value(); m_request_url = parsed_url.release_value();
// Set thiss synchronous flag if async is false; otherwise unset thiss synchronous flag. // 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: // 4. If body is not null, then:
if (body.has_value()) { if (body.has_value()) {
// 1. Let extractedContentType be null. // 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. // 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>>()) { 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. // 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: // 5. If originalAuthorContentType is non-null, then:
if (original_author_content_type.has_value()) { 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(); auto new_content_type_serialized = content_type_record->serialized();
// 3. Set (`Content-Type`, newContentTypeSerialized) in thiss author request headers. // 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)); 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. // 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. // 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()) { 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({ "Content-Type"sv, "text/html;charset=UTF-8"sv });
m_author_request_headers->set(move(header));
} }
// 2. Otherwise, if body is an XML document, set (`Content-Type`, `application/xml;charset=UTF-8`) in thiss author request headers. // 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()) { 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({ "Content-Type"sv, "application/xml;charset=UTF-8"sv });
m_author_request_headers->set(move(header));
} else { } else {
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
} }
// 3. Otherwise, if extractedContentType is not null, set (`Content-Type`, extractedContentType) in thiss author request headers. // 3. Otherwise, if extractedContentType is not null, set (`Content-Type`, extractedContentType) in thiss author request headers.
else if (extracted_content_type.has_value()) { 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)); m_author_request_headers->set(move(header));
} }
} }
@ -650,7 +643,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest
// method // method
// Thiss request method. // Thiss request method.
request->set_method(MUST(ByteBuffer::copy(m_request_method.bytes()))); request->set_method(m_request_method);
// URL // URL
// Thiss request 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 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. // 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()) if (!header_bytes.has_value())
return {}; 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 // 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. // 1. Let A be a, byte-uppercased.
auto uppercased_a = TRY(ByteBuffer::copy(a)); auto uppercased_a = a.to_uppercase();
Infra::byte_uppercase(uppercased_a);
// 2. Let B be b, byte-uppercased. // 2. Let B be b, byte-uppercased.
auto uppercased_b = TRY(ByteBuffer::copy(b)); auto uppercased_b = b.to_uppercase();
Infra::byte_uppercase(uppercased_b);
// 3. Return A is byte less than B. // 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 // 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(); 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. // 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: Unfortunately, this is needed for compatibility with deployed content.
// NOTE: quick_sort mutates the collection instead of returning a sorted copy.
quick_sort(initial_headers, [](Fetch::Infrastructure::Header const& a, Fetch::Infrastructure::Header const& b) { 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. // 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) { 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? // FIXME: The spec does not mention isomorphic decode. Spec bug?
auto decoder_header = Infra::isomorphic_decode(header.value); output.appendff("{}: {}\r\n", header.name, Infra::isomorphic_decode(header.value));
output.append(decoder_header.bytes());
output.append(0x0D); // '\r'
output.append(0x0A); // '\n'
} }
// 5. Return output. // 5. Return output.
@ -1157,10 +1141,8 @@ Fetch::Infrastructure::Status XMLHttpRequest::status() const
// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-statustext // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-statustext
WebIDL::ExceptionOr<String> XMLHttpRequest::status_text() const WebIDL::ExceptionOr<String> XMLHttpRequest::status_text() const
{ {
auto& vm = this->vm();
// The statusText getter steps are to return thiss responses status message. // 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 // 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> 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> 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); WebIDL::ExceptionOr<void> set_response_type(Bindings::XMLHttpRequestResponseType);
Optional<String> get_response_header(String const& name) const; Optional<String> get_response_header(String const& name) const;