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

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

View file

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