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

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