#include "headers.h" #include "simdutf.h" #include #include #include #include namespace workerd::api { namespace { #define COMMON_HEADERS(V) \ V(ACCEPT_CHARSET, "accept-charset") \ V(ACCEPT_ENCODING, "accept-encoding") \ V(ACCEPT_LANGUAGE, "accept-language") \ V(ACCEPT_RANGES, "accept-ranges") \ V(ACCEPT, "accept") \ V(ACCESS_CONTROL_ALLOW_ORIGIN, "access-control-allow-origin") \ V(AGE, "age") \ V(ALLOW, "allow") \ V(AUTHORIZATION, "authorization") \ V(CACHE_CONTROL, "cache-control") \ V(CONTENT_DISPOSITION, "content-disposition") \ V(CONTENT_ENCODING, "content-encoding") \ V(CONTENT_LANGUAGE, "content-language") \ V(CONTENT_LENGTH, "content-length") \ V(CONTENT_LOCATION, "content-location") \ V(CONTENT_RANGE, "content-range") \ V(CONTENT_TYPE, "content-type") \ V(COOKIE, "cookie") \ V(DATE, "date") \ V(ETAG, "etag") \ V(EXPECT, "expect") \ V(EXPIRES, "expires") \ V(FROM, "from") \ V(HOST, "host") \ V(IF_MATCH, "if-match") \ V(IF_MODIFIED_SINCE, "if-modified-since") \ V(IF_NONE_MATCH, "if-none-match") \ V(IF_RANGE, "if-range") \ V(IF_UNMODIFIED_SINCE, "if-unmodified-since") \ V(LAST_MODIFIED, "last-modified") \ V(LINK, "link") \ V(LOCATION, "location") \ V(MAX_FORWARDS, "max-forwards") \ V(PROXY_AUTHENTICATE, "proxy-authenticate") \ V(PROXY_AUTHORIZATION, "proxy-authorization") \ V(RANGE, "range") \ V(REFERER, "referer") \ V(REFRESH, "refresh") \ V(RETRY_AFTER, "retry-after") \ V(SERVER, "server") \ V(SET_COOKIE, "set-cookie") \ V(STRICT_TRANSPORT_SECURITY, "strict-transport-security") \ V(TRANSFER_ENCODING, "transfer-encoding") \ V(USER_AGENT, "user-agent") \ V(VARY, "vary") \ V(VIA, "via") \ V(WWW_AUTHENTICATE, "www-authenticate") // Constexpr array of lowercase common header names (must match CommonHeaderName enum order // and must be kept in sync with the ordinal values defined in http-over-capnp.capnp). Since // it is extremely unlikely that those will change often, we hardcode them here for runtime // efficiency. constexpr kj::StringPtr COMMON_HEADER_NAMES[] = {nullptr, // 0: invalid #define V(_, str) str##_kj, COMMON_HEADERS(V) #undef V }; // Static asserts for all of the common header names to ensure they are correct and in sync // with the enum values. static_assert(std::size(COMMON_HEADER_NAMES) == (Headers::MAX_COMMON_HEADER_ID + 1), "COMMON_HEADER_NAMES must have an entry for each CommonHeaderName enum value"); #define V(id, str) \ static_assert(COMMON_HEADER_NAMES[static_cast(capnp::CommonHeaderName::id)] == str##_kj); COMMON_HEADERS(V) #undef V inline constexpr kj::StringPtr getCommonHeaderName(uint id) { KJ_ASSERT(id > 0 && id <= Headers::MAX_COMMON_HEADER_ID, "Invalid common header ID"); return COMMON_HEADER_NAMES[id]; } // The CASE_CONVERSION_TABLE maps ASCII characters to their case conversion offset. For example, // 'A' (0x41) maps to 32, which is the offset to convert it to 'a' (0x61). This allows us to perform // branch-free case-insensitive comparisons and hashing by adding the offset to each character. // For non-ASCII characters, the offset is 0, which means they are not modified. This table is // used in the strcaseeq and caseInsensitiveHash functions below. // clang-format off constexpr kj::byte CASE_CONVERSION_TABLE[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; // clang-format on // Bit paranoid but just to be safe... static_assert(std::numeric_limits::max() < kj::size(CASE_CONVERSION_TABLE)); constexpr bool strcaseeq(kj::StringPtr a, kj::StringPtr b) { if (a.size() != b.size()) return false; size_t i = 0; while (i + sizeof(uint64_t) <= a.size()) { // We assume that a is always lower-case to start. uint64_t wa = (static_cast(static_cast(a[i + 0])) << 0) | (static_cast(static_cast(a[i + 1])) << 8) | (static_cast(static_cast(a[i + 2])) << 16) | (static_cast(static_cast(a[i + 3])) << 24) | (static_cast(static_cast(a[i + 4])) << 32) | (static_cast(static_cast(a[i + 5])) << 40) | (static_cast(static_cast(a[i + 6])) << 48) | (static_cast(static_cast(a[i + 7])) << 56); uint8_t b0 = b[i + 0]; uint8_t b1 = b[i + 1]; uint8_t b2 = b[i + 2]; uint8_t b3 = b[i + 3]; uint8_t b4 = b[i + 4]; uint8_t b5 = b[i + 5]; uint8_t b6 = b[i + 6]; uint8_t b7 = b[i + 7]; uint64_t normalized_b = (static_cast(b0 + CASE_CONVERSION_TABLE[b0]) << 0) | (static_cast(b1 + CASE_CONVERSION_TABLE[b1]) << 8) | (static_cast(b2 + CASE_CONVERSION_TABLE[b2]) << 16) | (static_cast(b3 + CASE_CONVERSION_TABLE[b3]) << 24) | (static_cast(b4 + CASE_CONVERSION_TABLE[b4]) << 32) | (static_cast(b5 + CASE_CONVERSION_TABLE[b5]) << 40) | (static_cast(b6 + CASE_CONVERSION_TABLE[b6]) << 48) | (static_cast(b7 + CASE_CONVERSION_TABLE[b7]) << 56); if (wa != normalized_b) return false; i += sizeof(uint64_t); } for (; i < a.size(); ++i) { char cb = b[i] + CASE_CONVERSION_TABLE[static_cast(b[i])]; if (a[i] != cb) return false; } return true; } // A FNV hash function that is case-insensitive. // See: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function constexpr uint caseInsensitiveHash(kj::StringPtr name) { static constexpr uint32_t MULTIPLIER = 16777619u; uint hash = 2166136261u; size_t i = 0; while (i + sizeof(uint64_t) <= name.size()) { uint8_t b0 = name[i + 0]; uint8_t b1 = name[i + 1]; uint8_t b2 = name[i + 2]; uint8_t b3 = name[i + 3]; uint8_t b4 = name[i + 4]; uint8_t b5 = name[i + 5]; uint8_t b6 = name[i + 6]; uint8_t b7 = name[i + 7]; hash ^= b0 + CASE_CONVERSION_TABLE[b0]; hash *= MULTIPLIER; hash ^= b1 + CASE_CONVERSION_TABLE[b1]; hash *= MULTIPLIER; hash ^= b2 + CASE_CONVERSION_TABLE[b2]; hash *= MULTIPLIER; hash ^= b3 + CASE_CONVERSION_TABLE[b3]; hash *= MULTIPLIER; hash ^= b4 + CASE_CONVERSION_TABLE[b4]; hash *= MULTIPLIER; hash ^= b5 + CASE_CONVERSION_TABLE[b5]; hash *= MULTIPLIER; hash ^= b6 + CASE_CONVERSION_TABLE[b6]; hash *= MULTIPLIER; hash ^= b7 + CASE_CONVERSION_TABLE[b7]; hash *= MULTIPLIER; i += sizeof(uint64_t); } // Handle remaining bytes for (; i < name.size(); ++i) { uint8_t b = name[i]; hash ^= b + CASE_CONVERSION_TABLE[b]; hash *= MULTIPLIER; } hash = (hash >> 16) ^ hash; return hash; } constexpr size_t HEADER_MAP_SIZE = 512; struct HeaderHashTable final { struct Entry { kj::StringPtr name; uint id = 0; }; Entry entries[HEADER_MAP_SIZE] = {}; // These are both calculated at compile time by the constexpr constructor. size_t kMaxEntrySize = 0; size_t kMinEntrySize = kj::maxValue; constexpr HeaderHashTable() { for (uint i = 1; i <= Headers::MAX_COMMON_HEADER_ID; ++i) { auto name = COMMON_HEADER_NAMES[i]; size_t slot = caseInsensitiveHash(name) % HEADER_MAP_SIZE; entries[slot] = {name, i}; kMaxEntrySize = std::max(kMaxEntrySize, name.size()); kMinEntrySize = std::min(kMinEntrySize, name.size()); } } constexpr uint find(kj::StringPtr name) const { if (name.size() < kMinEntrySize || name.size() > kMaxEntrySize) { return 0; } size_t slot = caseInsensitiveHash(name) % HEADER_MAP_SIZE; const auto& entry = entries[slot]; if (entry.id != 0 && strcaseeq(entry.name, name)) { return entry.id; } return 0; // Not found } // The isPerfectTest and isLowerTest are constexpr static assertions used // only the verify correctness of the hash table at compile time. constexpr bool isPerfectTest() const { for (uint i = 1; i <= Headers::MAX_COMMON_HEADER_ID; ++i) { auto name = COMMON_HEADER_NAMES[i]; size_t slot = caseInsensitiveHash(name) % HEADER_MAP_SIZE; if (entries[slot].id != i) return false; if (!strcaseeq(entries[slot].name, name)) return false; } return true; } constexpr bool isLowerTest() const { for (uint i = 1; i <= Headers::MAX_COMMON_HEADER_ID; ++i) { auto name = COMMON_HEADER_NAMES[i]; for (char c: name) { if ('A' <= c && c <= 'Z') return false; } } return true; } }; constexpr HeaderHashTable HEADER_HASH_TABLE; static_assert(HEADER_HASH_TABLE.kMaxEntrySize == 27); static_assert(HEADER_HASH_TABLE.kMinEntrySize == 3); static_assert(HEADER_HASH_TABLE.isPerfectTest()); static_assert(HEADER_HASH_TABLE.isLowerTest()); static_assert(HEADER_HASH_TABLE.find("accept-charset"_kj) == 1); static_assert(HEADER_HASH_TABLE.find("AcCePt-ChArSeT"_kj) == 1); static_assert(std::size(COMMON_HEADER_NAMES) == (Headers::MAX_COMMON_HEADER_ID + 1)); void maybeWarnIfBadHeaderString(kj::StringPtr name, kj::StringPtr str) { KJ_IF_SOME(context, IoContext::tryCurrent()) { if (context.hasWarningHandler()) { if (!simdutf::validate_ascii(str.begin(), str.size())) { // The string contains non-ASCII characters. While any 8-bit value is technically valid // in HTTP headers, we encode header strings as UTF-8, so we want to warn the user that // their header name/value may not be what they may expect based on what browsers do. auto utf8Hex = kj::strArray( KJ_MAP(b, str) { return kj::str("\\x", kj::hex(static_cast(b))); }, ""); context.logWarning(kj::str("A header value for \"", name, "\" contains non-ASCII " "characters: \"", str, "\" (raw bytes: \"", utf8Hex, "\"). As a quirk to support Unicode, we are encoding " "values as UTF-8 in the header, but in a browser this would likely result in a " "TypeError exception. Consider encoding this string in ASCII for compatibility with " "browser implementations of the Fetch specification.")); } } } } // Left- and right-trim HTTP whitespace from `value`. kj::String normalizeHeaderValue(kj::StringPtr name, kj::String value) { // Fast path: if empty, return as-is if (value.size() == 0) { return kj::mv(value); } char* begin = value.begin(); char* end = value.end(); while (begin < end && util::isHttpWhitespace(*begin)) { ++begin; } while (begin < end && util::isHttpWhitespace(*(end - 1))) { --end; } size_t newSize = end - begin; if (newSize == value.size()) { JSG_REQUIRE(workerd::util::isValidHeaderValue(value), TypeError, "Invalid header value."); maybeWarnIfBadHeaderString(name, value); return kj::mv(value); } auto trimmed = kj::ArrayPtr(begin, newSize); JSG_REQUIRE(workerd::util::isValidHeaderValue(trimmed), TypeError, "Invalid header value."); maybeWarnIfBadHeaderString(name, value); // By attaching the original array to the trimmed view, we keep the original allocation alive // and prevent an unnecessary copy. return kj::str(trimmed.attach(value.releaseArray())); } Headers::HeaderKey getHeaderKeyFor(kj::StringPtr name) { if (uint commonId = HEADER_HASH_TABLE.find(name)) { KJ_DASSERT(commonId > 0 && commonId <= Headers::MAX_COMMON_HEADER_ID); return commonId; } for (char c: name) { JSG_REQUIRE(util::isHttpTokenChar(c), TypeError, "Invalid header name."); } // Not a common header, so allocate lowercase copy for uncommon header return toLower(name); } } // namespace Headers::Headers(jsg::Lock& js, jsg::Dict dict): guard(Guard::NONE) { // Because the headers might end up in either of our two tables, // we can't really reserve space for them up front. for (auto& field: dict.fields) { append(js, kj::mv(field.name), kj::mv(field.value)); } } Headers::Headers(jsg::Lock& js, const Headers& other): guard(Guard::NONE) { for (kj::uint i = 1; i < other.commonHeaders.size(); i++) { commonHeaders[i] = other.commonHeaders[i].map([](const kj::Own
& h) { return h->clone(); }); } uncommonHeaders.reserve(other.uncommonHeaders.size()); for (auto& [key, header]: other.uncommonHeaders) { // It should not be possible to have duplicate keys here. uncommonHeaders.insert(kj::str(key), header->clone()); } } Headers::Headers(jsg::Lock& js, const kj::HttpHeaders& other, Guard guard): guard(guard) { // TODO(perf): Once kj::HttpHeaders supports an API for getting the CommonHeaderName directly // from the headers, we can optimize this to avoid looking up the common header IDs again, // making this constructor more efficient when copying common headers from kj::HttpHeaders. other.forEach([this, &js](auto name, auto value) { // We have to copy the strings here but we can avoid normalizing and validating since // they presumably already went through that process when they were added to the // kj::HttpHeader instance. appendUnguarded(js, kj::str(name), kj::str(value)); }); } kj::Maybe Headers::tryGetHeader(const HeaderKey& key) { KJ_SWITCH_ONEOF(key) { KJ_CASE_ONEOF(idx, kj::uint) { return commonHeaders[idx].map([](kj::Own
& header) -> Header& { return *header; }); } KJ_CASE_ONEOF(name, kj::String) { return uncommonHeaders.find(name).map( [](kj::Own
& header) -> Header& { return *header; }); } } KJ_UNREACHABLE; } jsg::Ref Headers::clone(jsg::Lock& js) const { auto result = js.alloc(js, *this); result->guard = guard; return kj::mv(result); } // Fill in the given HttpHeaders with these headers. Note that strings are inserted by // reference, so the output must be consumed immediately. void Headers::shallowCopyTo(kj::HttpHeaders& out) { for (kj::uint i = 1; i < commonHeaders.size(); i++) { KJ_IF_SOME(header, commonHeaders[i]) { KJ_IF_SOME(name, header->name) { for (auto& value: header->values) { out.addPtrPtr(name, value); } } else { auto name = getCommonHeaderName(i); for (auto& value: header->values) { out.addPtrPtr(name, value); } } } } // This is unfortunate... but we have to sort the uncommon headers by name // before passing them off to kj::HttpHeaders...While the actual header order // (for different header names) is not meaningful in HTTP, we have tests that // expect a certain order for reproducibility. struct Entry { kj::StringPtr name; kj::StringPtr value; }; kj::Vector entries(uncommonHeaders.size()); for (auto& header: uncommonHeaders) { KJ_IF_SOME(name, header.value->name) { for (auto& value: header.value->values) { entries.add(Entry{.name = name, .value = value}); } } else { for (auto& value: header.value->values) { entries.add(Entry{.name = header.key, .value = value}); } } } std::stable_sort(entries.begin(), entries.end(), [](const Entry& a, const Entry& b) { return a.name < b.name; }); for (const auto& entry: entries) { out.addPtrPtr(entry.name, entry.value); } } kj::Array Headers::getDisplayedHeaders(jsg::Lock& js) { auto getSetCookie = FeatureFlags::get(js).getHttpHeadersGetSetCookie(); size_t reserved = 0; for (kj::uint i = 1; i < commonHeaders.size(); i++) { KJ_IF_SOME(header, commonHeaders[i]) { if (getSetCookie && i == static_cast(capnp::CommonHeaderName::SET_COOKIE)) { reserved += header->values.size(); } else { reserved += 1; } } } for (auto& header: uncommonHeaders) { reserved += header.value->values.size(); } kj::Vector vec(reserved); for (kj::uint i = 1; i < commonHeaders.size(); i++) { auto name = getCommonHeaderName(i); KJ_IF_SOME(header, commonHeaders[i]) { if (getSetCookie && i == static_cast(capnp::CommonHeaderName::SET_COOKIE)) { for (auto& value: header->values) { vec.add(Headers::DisplayedHeader{ .key = kj::str(name), .value = kj::str(value), }); } } else { vec.add(Headers::DisplayedHeader{ .key = kj::str(name), .value = kj::strArray(header->values, ", "), }); } } } for (auto& header: uncommonHeaders) { vec.add(Headers::DisplayedHeader{ .key = kj::str(header.key), .value = kj::strArray(header.value->values, ", "), }); } auto ret = vec.releaseAsArray(); std::sort(ret.begin(), ret.end(), [](const auto& a, const auto& b) { return a.key < b.key; }); return kj::mv(ret); } jsg::Ref Headers::constructor(jsg::Lock& js, jsg::Optional init) { using StringDict = jsg::Dict; KJ_IF_SOME(i, init) { KJ_SWITCH_ONEOF(kj::mv(i)) { KJ_CASE_ONEOF(dict, StringDict) { return js.alloc(js, kj::mv(dict)); } KJ_CASE_ONEOF(headers, jsg::Ref) { return js.alloc(js, *headers); // It's important to note here that we are treating the Headers object // as a special case here. Per the fetch spec, we *should* be grabbing // the Symbol.iterator off the Headers object and interpreting it as // a Sequence> (as in the StringPairs case // below). However, special casing Headers like we do here is more // performant and has other side effects such as preserving the casing // of header names that have been received. // // This does mean that we fail one of the more pathological (and kind // of weird) Web Platform Tests for this API: // // const h = new Headers(); // h[Symbol.iterator] = function * () { yield ["test", "test"]; }; // const headers = new Headers(h); // console.log(headers.has("test")); // // The spec would say headers.has("test") here should be true. With our // implementation here, however, we are ignoring the Symbol.iterator so // the test fails. } KJ_CASE_ONEOF(pairs, StringPairs) { auto dict = KJ_MAP(entry, pairs) { JSG_REQUIRE(entry.size() == 2, TypeError, "To initialize a Headers object from a sequence, each inner sequence " "must have exactly two elements."); return StringDict::Field{kj::mv(entry[0]), kj::mv(entry[1])}; }; return js.alloc(js, StringDict{kj::mv(dict)}); } } } return js.alloc(); } kj::Maybe Headers::get(jsg::Lock& js, kj::String name) { return getPtr(js, name); } kj::Maybe Headers::getPtr(jsg::Lock& js, kj::StringPtr name) { return tryGetHeader(getHeaderKeyFor(name)).map([](Header& header) { return kj::strArray(header.values, ", "); }); } kj::Maybe Headers::getCommon(jsg::Lock& js, capnp::CommonHeaderName idx) { kj::uint index = static_cast(idx); KJ_DASSERT(index <= Headers::MAX_COMMON_HEADER_ID); return commonHeaders[index].map([](auto& header) { return kj::strArray(header->values, ", "); }); } kj::Array Headers::getSetCookie() { auto& header = commonHeaders[static_cast(capnp::CommonHeaderName::SET_COOKIE)]; KJ_IF_SOME(h, header) { return KJ_MAP(value, h->values) { return value.asPtr(); }; } return nullptr; } kj::Array Headers::getAll(kj::String name) { if (!strcaseeq("set-cookie"_kj, name)) { JSG_FAIL_REQUIRE(TypeError, "getAll() can only be used with the header name \"Set-Cookie\"."); } // getSetCookie() is the standard API here. getAll(...) is our legacy non-standard extension // for the same use case. We continue to support getAll for backwards compatibility but moving // forward users really should be using getSetCookie. return getSetCookie(); } bool Headers::has(kj::String name) { return tryGetHeader(getHeaderKeyFor(name)) != kj::none; } bool Headers::hasCommon(capnp::CommonHeaderName idx) { kj::uint index = static_cast(idx); KJ_DASSERT(index <= Headers::MAX_COMMON_HEADER_ID); return commonHeaders[index] != kj::none; } void Headers::set(jsg::Lock& js, kj::String name, kj::String value) { checkGuard(); value = normalizeHeaderValue(name, kj::mv(value)); setUnguarded(js, kj::mv(name), kj::mv(value)); } void Headers::setUnguarded(jsg::Lock& js, kj::String name, kj::String value) { KJ_SWITCH_ONEOF(getHeaderKeyFor(name)) { KJ_CASE_ONEOF(id, kj::uint) { KJ_IF_SOME(existing, commonHeaders[id]) { existing->values.resize(1); existing->values[0] = kj::mv(value); } else { auto& created = commonHeaders[id].emplace(kj::heap(Header())); if (name != getCommonHeaderName(id)) { created->name = kj::mv(name); } created->values.resize(1); created->values[0] = kj::mv(value); } return; } KJ_CASE_ONEOF(n, kj::String) { using Ret = decltype(uncommonHeaders)::Entry; auto& header = uncommonHeaders.findOrCreate(n, [&] -> Ret { kj::Maybe maybeName; if (name != n) { maybeName = kj::mv(name); } return Ret{ .key = kj::mv(n), .value = kj::heap(Header(kj::mv(maybeName))), }; }); header->values.resize(1); header->values[0] = kj::mv(value); return; } } KJ_UNREACHABLE; } void Headers::setCommon(capnp::CommonHeaderName idx, kj::String value) { kj::uint index = static_cast(idx); KJ_DASSERT(index <= Headers::MAX_COMMON_HEADER_ID); KJ_IF_SOME(existing, commonHeaders[index]) { existing->values.resize(1); existing->values[0] = kj::mv(value); } else { auto& created = commonHeaders[index].emplace(kj::heap(Header())); created->values.resize(1); created->values[0] = kj::mv(value); } } void Headers::append(jsg::Lock& js, kj::String name, kj::String value) { checkGuard(); value = normalizeHeaderValue(name, kj::mv(value)); appendUnguarded(js, kj::mv(name), kj::mv(value)); } void Headers::appendUnguarded(jsg::Lock& js, kj::String name, kj::String value) { KJ_SWITCH_ONEOF(getHeaderKeyFor(name)) { KJ_CASE_ONEOF(id, kj::uint) { KJ_IF_SOME(existing, commonHeaders[id]) { existing->values.add(kj::mv(value)); } else { auto& created = commonHeaders[id].emplace(kj::heap(Header())); if (name != getCommonHeaderName(id)) { created->name = kj::mv(name); } created->values.resize(1); created->values[0] = kj::mv(value); } return; } KJ_CASE_ONEOF(n, kj::String) { KJ_IF_SOME(existing, uncommonHeaders.find(n)) { existing->values.add(kj::mv(value)); } else { using Ret = decltype(uncommonHeaders)::Entry; auto& header = uncommonHeaders.findOrCreate(n, [&] -> Ret { kj::Maybe maybeName; if (name != n) { maybeName = kj::mv(name); } return Ret{ .key = kj::mv(n), .value = kj::heap(Header(kj::mv(maybeName))), }; }); header->values.add(kj::mv(value)); } return; } } KJ_UNREACHABLE; } void Headers::delete_(kj::String name) { checkGuard(); KJ_SWITCH_ONEOF(getHeaderKeyFor(name)) { KJ_CASE_ONEOF(id, kj::uint) { commonHeaders[id] = kj::none; return; } KJ_CASE_ONEOF(n, kj::String) { uncommonHeaders.erase(n); return; } } KJ_UNREACHABLE; } void Headers::deleteCommon(capnp::CommonHeaderName idx) { kj::uint index = static_cast(idx); KJ_DASSERT(index <= Headers::MAX_COMMON_HEADER_ID); commonHeaders[index] = kj::none; } // There are a couple implementation details of the Headers iterators worth calling out. // // 1. Each iterator gets its own copy of the keys and/or values of the headers. While nauseating // from a performance perspective, this solves both the iterator -> iterable lifetime dependence // and the iterator invalidation issue: i.e., it's impossible for a user to unsafely modify the // Headers data structure while iterating over it, because they are simply two separate data // structures. By empirical testing, this seems to be how Chrome implements Headers iteration. // // Other alternatives bring their own pitfalls. We could store a Ref of the parent Headers // object, solving the lifetime issue. To solve the iterator invalidation issue, we could store a // copy of the currently-iterated-over key and use std::upper_bound() to find the next entry // every time we want to increment the iterator (making the increment operation O(lg n) rather // than O(1)); or we could make each Header entry in the map store a set of back-pointers to all // live iterators pointing to it, with delete_() incrementing all iterators in the set whenever // it deletes a header entry. Neither hack appealed to me. // // 2. Notice that the next() member function of the iterator classes moves the string(s) they // contain, rather than making a copy of them as in the FormData iterators. This is safe to do // because, unlike FormData, these iterators have their own copies of the strings, and since they // are forward-only iterators, we know we won't need the strings again. // // TODO(perf): On point 1, perhaps we could avoid most copies by using a copy-on-write strategy // applied to the header map elements? We'd still copy the whole data structure to avoid iterator // invalidation, but the elements would be cheaper to copy. jsg::Ref Headers::entries(jsg::Lock& js) { return js.alloc(IteratorState{getDisplayedHeaders(js)}); } jsg::Ref Headers::keys(jsg::Lock& js) { auto headers = getDisplayedHeaders(js); kj::Vector keys(headers.size()); for (auto& header: headers) { keys.add(kj::mv(header.key)); }; return js.alloc(IteratorState(keys.releaseAsArray())); } jsg::Ref Headers::values(jsg::Lock& js) { // Annoyingly, the spec requires that the values iterator still be sorted by key. // To make this easiest, let's grab the displayed headers and then extract the values. // the getDisplayedHeaders() function does the sorting for us at the cost of an extra // copy of the names. Fortunately, enumerating by value is likely way less common than // other forms of iteration so the cost should be acceptable. auto headers = getDisplayedHeaders(js); kj::Vector values(headers.size()); for (auto& header: headers) { values.add(kj::mv(header.value)); }; return js.alloc(IteratorState(values.releaseAsArray())); } void Headers::forEach(jsg::Lock& js, jsg::Function)> callback, jsg::Optional thisArg) { auto receiver = js.v8Undefined(); KJ_IF_SOME(arg, thisArg) { auto handle = arg.getHandle(js); if (!handle->IsNullOrUndefined()) { receiver = handle; } } callback.setReceiver(js.v8Ref(receiver)); for (auto& entry: getDisplayedHeaders(js)) { callback(js, entry.value, entry.key, JSG_THIS); } } bool Headers::inspectImmutable() { return guard != Guard::NONE; } void Headers::visitForMemoryInfo(jsg::MemoryTracker& tracker) const { for (const auto& header: commonHeaders) { tracker.trackField("header", header); } for (const auto& header: uncommonHeaders) { tracker.trackField(nullptr, header.value); } } // ----------------------------------------------------------------------------- // serialization of headers // // http-over-capnp.capnp has a nice list of common header names, taken from the HTTP/2 standard. // We'll use it as an optimization. // // Note that using numeric IDs for headers implies we lose the original capitalization. However, // the JS Headers API doesn't actually give the application any way to observe the capitalization // of header names -- it only becomes relevant when serializing over HTTP/1.1. And at that point, // we are actually free to change the capitalization anyway, and we commonly do (KJ itself will // normalize capitalization of all registered headers, and http-over-capnp also loses // capitalization). So, it's certainly not worth it to try to keep the original capitalization // across serialization. void Headers::serialize(jsg::Lock& js, jsg::Serializer& serializer) { // We serialize as a series of key-value pairs. Each value is a length-delimited string. Each key // is a common header ID, or the value zero to indicate an uncommon header, which is then // followed by a length-delimited name. serializer.writeRawUint32(static_cast(guard)); // Write the count of headers. uint count = 0; for (auto& header: commonHeaders) { KJ_IF_SOME(h, header) { count += h->values.size(); } } for (auto& header: uncommonHeaders) { count += header.value->values.size(); } serializer.writeRawUint32(count); // Now write key/values. for (kj::uint i = 1; i < commonHeaders.size(); i++) { KJ_IF_SOME(header, commonHeaders[i]) { for (auto& value: header->values) { serializer.writeRawUint32(i); serializer.writeLengthDelimited(value); } } } for (auto& header: uncommonHeaders) { auto name = ([&] -> kj::StringPtr { KJ_IF_SOME(name, header.value->name) { return name; } else { return header.key; } })(); for (auto& value: header.value->values) { serializer.writeRawUint32(0); serializer.writeLengthDelimited(name); serializer.writeLengthDelimited(value); } } } jsg::Ref Headers::deserialize( jsg::Lock& js, rpc::SerializationTag tag, jsg::Deserializer& deserializer) { auto result = js.alloc(); uint guard = deserializer.readRawUint32(); KJ_REQUIRE(guard <= static_cast(Guard::NONE), "unknown guard value"); uint count = deserializer.readRawUint32(); JSG_REQUIRE(count <= 1024, DOMDataCloneError, "Deserialization failed: header count exceeds maximum (", count, ")"); for (auto i KJ_UNUSED: kj::zeroTo(count)) { uint commonId = deserializer.readRawUint32(); kj::String name; if (commonId == 0) { name = deserializer.readLengthDelimitedString(); } else { KJ_ASSERT(commonId <= Headers::MAX_COMMON_HEADER_ID); name = kj::str(getCommonHeaderName(commonId)); } auto value = deserializer.readLengthDelimitedString(); // TODO(performance): We can avoid some copies here by constructing the // the Header entry directly using information from the deserializer // directly without relying on append. result->appendUnguarded(js, kj::mv(name), kj::mv(value)); } // Don't actually set the guard until here because it may block the ability to call `append()`. result->guard = static_cast(guard); return result; } } // namespace workerd::api