diff options
-rw-r--r-- | packages/bun-types/globals.d.ts | 36 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/FetchHeaders.cpp | 53 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/FetchHeaders.h | 3 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/HTTPHeaderMap.cpp | 82 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/HTTPHeaderMap.h | 178 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/HTTPHeaderNames.h | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSFetchHeaders.cpp | 140 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSFetchHeaders.h | 1 | ||||
-rw-r--r-- | test/bun.js/serve.test.ts | 25 |
9 files changed, 450 insertions, 73 deletions
diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index e3b2e6a3e..8c4ae5ab4 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -342,6 +342,42 @@ interface Headers { * Get the total number of headers */ readonly count: number; + + /** + * Get all headers matching the name + * + * Only supports `"Set-Cookie"`. All other headers are empty arrays. + * + * @param name - The header name to get + * + * @returns An array of header values + * + * @example + * ```ts + * const headers = new Headers(); + * headers.append("Set-Cookie", "foo=bar"); + * headers.append("Set-Cookie", "baz=qux"); + * headers.getAll("Set-Cookie"); // ["foo=bar", "baz=qux"] + * ``` + */ + getAll(name: string): string[]; + + /** + * Returns the `Set-Cookie` header as an array of strings + * + * Based on https://github.com/whatwg/fetch/pull/1346 + * + * @returns An array of `Set-Cookie` header values + * + * @example + * ```ts + * const headers = new Headers(); + * headers.append("Set-Cookie", "foo=bar"); + * headers.append("Set-Cookie", "baz=qux"); + * headers.getSetCookie(); // ["foo=bar", "baz=qux"] + * ``` + */ + getSetCookie(): string[]; } declare var Headers: { diff --git a/src/bun.js/bindings/webcore/FetchHeaders.cpp b/src/bun.js/bindings/webcore/FetchHeaders.cpp index 713b8a6ac..7834836c7 100644 --- a/src/bun.js/bindings/webcore/FetchHeaders.cpp +++ b/src/bun.js/bindings/webcore/FetchHeaders.cpp @@ -39,6 +39,18 @@ static void removePrivilegedNoCORSRequestHeaders(HTTPHeaderMap& headers) headers.remove(HTTPHeaderName::Range); } +static ExceptionOr<bool> canWriteHeader(const HTTPHeaderName name, const String& value, const String& combinedValue, FetchHeaders::Guard guard) +{ + ASSERT(value.isEmpty() || (!isHTTPSpace(value[0]) && !isHTTPSpace(value[value.length() - 1]))); + if (!isValidHTTPHeaderValue((value))) + return Exception { TypeError, makeString("Header '", name, "' has invalid value: '", value, "'") }; + if (guard == FetchHeaders::Guard::Immutable) + return Exception { TypeError, "Headers object's guard is 'immutable'"_s }; + if (guard == FetchHeaders::Guard::RequestNoCors && !combinedValue.isEmpty()) + return false; + return true; +} + static ExceptionOr<bool> canWriteHeader(const String& name, const String& value, const String& combinedValue, FetchHeaders::Guard guard) { if (!isValidHTTPToken(name)) @@ -61,6 +73,31 @@ static ExceptionOr<void> appendToHeaderMap(const String& name, const String& val { String normalizedValue = stripLeadingAndTrailingHTTPSpaces(value); String combinedValue = normalizedValue; + HTTPHeaderName headerName; + if (findHTTPHeaderName(name, headerName)) { + + if (headerName != HTTPHeaderName::SetCookie) { + if (headers.contains(headerName)) { + combinedValue = makeString(headers.get(headerName), ", ", normalizedValue); + } + } + + auto canWriteResult = canWriteHeader(headerName, normalizedValue, combinedValue, guard); + + if (canWriteResult.hasException()) + return canWriteResult.releaseException(); + if (!canWriteResult.releaseReturnValue()) + return {}; + + if (headerName != HTTPHeaderName::SetCookie) { + headers.set(headerName, combinedValue); + } else { + headers.add(headerName, normalizedValue); + } + + return {}; + } + if (headers.contains(name)) combinedValue = makeString(headers.get(name), ", ", normalizedValue); auto canWriteResult = canWriteHeader(name, normalizedValue, combinedValue, guard); @@ -222,28 +259,42 @@ void FetchHeaders::filterAndFill(const HTTPHeaderMap& headers, Guard guard) } } +static NeverDestroyed<const String> setCookieLowercaseString(MAKE_STATIC_STRING_IMPL("set-cookie")); + std::optional<KeyValuePair<String, String>> FetchHeaders::Iterator::next() { if (m_keys.isEmpty() || m_updateCounter != m_headers->m_updateCounter) { m_keys.resize(0); m_keys.reserveCapacity(m_headers->m_headers.size()); for (auto& header : m_headers->m_headers) - m_keys.uncheckedAppend(header.key.convertToASCIILowercase()); + m_keys.uncheckedAppend(header.asciiLowerCaseName()); std::sort(m_keys.begin(), m_keys.end(), WTF::codePointCompareLessThan); m_updateCounter = m_headers->m_updateCounter; + m_cookieIndex = 0; } + + auto& setCookieHeaders = m_headers->m_headers.getSetCookieHeaders(); + while (m_currentIndex < m_keys.size()) { auto key = m_keys[m_currentIndex++]; + + if (!setCookieHeaders.isEmpty() && key == setCookieLowercaseString) { + auto cookie = setCookieHeaders[m_cookieIndex++]; + return KeyValuePair<String, String> { WTFMove(key), WTFMove(cookie) }; + } + auto value = m_headers->m_headers.get(key); if (!value.isNull()) return KeyValuePair<String, String> { WTFMove(key), WTFMove(value) }; } + return std::nullopt; } FetchHeaders::Iterator::Iterator(FetchHeaders& headers) : m_headers(headers) { + m_cookieIndex = 0; } } // namespace WebCore diff --git a/src/bun.js/bindings/webcore/FetchHeaders.h b/src/bun.js/bindings/webcore/FetchHeaders.h index 123833fa0..81f8e89c2 100644 --- a/src/bun.js/bindings/webcore/FetchHeaders.h +++ b/src/bun.js/bindings/webcore/FetchHeaders.h @@ -72,6 +72,8 @@ public: bool fastRemove(HTTPHeaderName name) { return m_headers.remove(name); } void fastSet(HTTPHeaderName name, const String& value) { m_headers.set(name, value); } + const Vector<String, 0>& getSetCookieHeaders() const { return m_headers.getSetCookieHeaders(); } + class Iterator { public: explicit Iterator(FetchHeaders&); @@ -82,6 +84,7 @@ public: size_t m_currentIndex { 0 }; Vector<String> m_keys; uint64_t m_updateCounter { 0 }; + size_t m_cookieIndex { 0 }; }; Iterator createIterator() { return Iterator { *this }; } diff --git a/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp b/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp index 383b0a85f..580a171d3 100644 --- a/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp +++ b/src/bun.js/bindings/webcore/HTTPHeaderMap.cpp @@ -36,17 +36,26 @@ #include <wtf/CrossThreadCopier.h> #include <wtf/text/StringView.h> +static StringView extractCookieName(const StringView& cookie) +{ + auto nameEnd = cookie.find('='); + if (nameEnd == notFound) + return String(); + return cookie.substring(0, nameEnd); +} + namespace WebCore { HTTPHeaderMap::HTTPHeaderMap() { } -HTTPHeaderMap HTTPHeaderMap::isolatedCopy() const & +HTTPHeaderMap HTTPHeaderMap::isolatedCopy() const& { HTTPHeaderMap map; map.m_commonHeaders = crossThreadCopy(m_commonHeaders); map.m_uncommonHeaders = crossThreadCopy(m_uncommonHeaders); + map.m_setCookieHeaders = crossThreadCopy(m_setCookieHeaders); return map; } @@ -55,6 +64,7 @@ HTTPHeaderMap HTTPHeaderMap::isolatedCopy() && HTTPHeaderMap map; map.m_commonHeaders = crossThreadCopy(WTFMove(m_commonHeaders)); map.m_uncommonHeaders = crossThreadCopy(WTFMove(m_uncommonHeaders)); + map.m_setCookieHeaders = crossThreadCopy(WTFMove(m_setCookieHeaders)); return map; } @@ -153,10 +163,14 @@ void HTTPHeaderMap::append(const String& name, const String& value) ASSERT(!contains(name)); HTTPHeaderName headerName; - if (findHTTPHeaderName(name, headerName)) - m_commonHeaders.append(CommonHeader { headerName, value }); - else + if (findHTTPHeaderName(name, headerName)) { + if (headerName == HTTPHeaderName::SetCookie) + m_setCookieHeaders.append(value); + else + m_commonHeaders.append(CommonHeader { headerName, value }); + } else { m_uncommonHeaders.append(UncommonHeader { name, value }); + } } bool HTTPHeaderMap::addIfNotPresent(HTTPHeaderName headerName, const String& value) @@ -181,6 +195,7 @@ bool HTTPHeaderMap::contains(const String& name) const bool HTTPHeaderMap::remove(const String& name) { + HTTPHeaderName headerName; if (findHTTPHeaderName(name, headerName)) return remove(headerName); @@ -192,6 +207,26 @@ bool HTTPHeaderMap::remove(const String& name) String HTTPHeaderMap::get(HTTPHeaderName name) const { + if (name == HTTPHeaderName::SetCookie) { + unsigned count = m_setCookieHeaders.size(); + switch (count) { + case 0: + return String(); + case 1: + return m_setCookieHeaders[0]; + default: { + StringBuilder builder; + builder.reserveCapacity(m_setCookieHeaders[0].length() * count + (count - 1)); + builder.append(m_setCookieHeaders[0]); + for (unsigned i = 1; i < count; ++i) { + builder.append(", "_s); + builder.append(m_setCookieHeaders[i]); + } + return builder.toString(); + } + } + } + auto index = m_commonHeaders.findIf([&](auto& header) { return header.key == name; }); @@ -200,6 +235,20 @@ String HTTPHeaderMap::get(HTTPHeaderName name) const void HTTPHeaderMap::set(HTTPHeaderName name, const String& value) { + if (name == HTTPHeaderName::SetCookie) { + auto cookieName = extractCookieName(value); + size_t length = m_setCookieHeaders.size(); + const auto& cookies = m_setCookieHeaders.data(); + for (size_t i = 0; i < length; ++i) { + if (extractCookieName(cookies[i]) == cookieName) { + m_setCookieHeaders[i] = value; + return; + } + } + m_setCookieHeaders.append(value); + return; + } + auto index = m_commonHeaders.findIf([&](auto& header) { return header.key == name; }); @@ -211,6 +260,9 @@ void HTTPHeaderMap::set(HTTPHeaderName name, const String& value) bool HTTPHeaderMap::contains(HTTPHeaderName name) const { + if (name == HTTPHeaderName::SetCookie) + return !m_setCookieHeaders.isEmpty(); + return m_commonHeaders.findIf([&](auto& header) { return header.key == name; }) != notFound; @@ -218,6 +270,12 @@ bool HTTPHeaderMap::contains(HTTPHeaderName name) const bool HTTPHeaderMap::remove(HTTPHeaderName name) { + if (name == HTTPHeaderName::SetCookie) { + bool any = m_setCookieHeaders.size() > 0; + m_setCookieHeaders.clear(); + return any; + } + return m_commonHeaders.removeFirstMatching([&](auto& header) { return header.key == name; }); @@ -225,6 +283,22 @@ bool HTTPHeaderMap::remove(HTTPHeaderName name) void HTTPHeaderMap::add(HTTPHeaderName name, const String& value) { + if (name == HTTPHeaderName::SetCookie) { + auto cookieName = extractCookieName(value); + + size_t length = m_setCookieHeaders.size(); + const auto& cookies = m_setCookieHeaders.data(); + for (size_t i = 0; i < length; ++i) { + if (extractCookieName(cookies[i]) == cookieName) { + m_setCookieHeaders[i] = value; + return; + } + } + m_setCookieHeaders.append(value); + + return; + } + auto index = m_commonHeaders.findIf([&](auto& header) { return header.key == name; }); diff --git a/src/bun.js/bindings/webcore/HTTPHeaderMap.h b/src/bun.js/bindings/webcore/HTTPHeaderMap.h index 0b4242b57..0e4d9b565 100644 --- a/src/bun.js/bindings/webcore/HTTPHeaderMap.h +++ b/src/bun.js/bindings/webcore/HTTPHeaderMap.h @@ -21,7 +21,7 @@ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once @@ -40,24 +40,24 @@ public: HTTPHeaderName key; String value; - CommonHeader isolatedCopy() const & { return { key , value.isolatedCopy() }; } - CommonHeader isolatedCopy() && { return { key , WTFMove(value).isolatedCopy() }; } - template <class Encoder> void encode(Encoder&) const; - template <class Decoder> static std::optional<CommonHeader> decode(Decoder&); + CommonHeader isolatedCopy() const & { return { key, value.isolatedCopy() }; } + CommonHeader isolatedCopy() && { return { key, WTFMove(value).isolatedCopy() }; } + template<class Encoder> void encode(Encoder &) const; + template<class Decoder> static std::optional<CommonHeader> decode(Decoder &); - bool operator==(const CommonHeader& other) const { return key == other.key && value == other.value; } + bool operator==(const CommonHeader &other) const { return key == other.key && value == other.value; } }; struct UncommonHeader { String key; String value; - UncommonHeader isolatedCopy() const & { return { key.isolatedCopy() , value.isolatedCopy() }; } - UncommonHeader isolatedCopy() && { return { WTFMove(key).isolatedCopy() , WTFMove(value).isolatedCopy() }; } - template <class Encoder> void encode(Encoder&) const; - template <class Decoder> static std::optional<UncommonHeader> decode(Decoder&); + UncommonHeader isolatedCopy() const & { return { key.isolatedCopy(), value.isolatedCopy() }; } + UncommonHeader isolatedCopy() && { return { WTFMove(key).isolatedCopy(), WTFMove(value).isolatedCopy() }; } + template<class Encoder> void encode(Encoder &) const; + template<class Decoder> static std::optional<UncommonHeader> decode(Decoder &); - bool operator==(const UncommonHeader& other) const { return key == other.key && value == other.value; } + bool operator==(const UncommonHeader &other) const { return key == other.key && value == other.value; } }; typedef Vector<CommonHeader, 0, CrashOnOverflow, 6> CommonHeadersVector; @@ -65,45 +65,65 @@ public: class HTTPHeaderMapConstIterator { public: - HTTPHeaderMapConstIterator(const HTTPHeaderMap& table, CommonHeadersVector::const_iterator commonHeadersIt, UncommonHeadersVector::const_iterator uncommonHeadersIt) + HTTPHeaderMapConstIterator(const HTTPHeaderMap &table, CommonHeadersVector::const_iterator commonHeadersIt, UncommonHeadersVector::const_iterator uncommonHeadersIt, Vector<String, 0>::const_iterator setCookiesIter) : m_table(table) , m_commonHeadersIt(commonHeadersIt) , m_uncommonHeadersIt(uncommonHeadersIt) + , m_setCookiesIter(setCookiesIter) { - if (!updateKeyValue(m_commonHeadersIt)) - updateKeyValue(m_uncommonHeadersIt); + if (!updateKeyValue(m_commonHeadersIt)) { + if (!updateSetCookieHeaderPosition(setCookiesIter)) { + updateKeyValue(m_uncommonHeadersIt); + } + } } struct KeyValue { String key; std::optional<HTTPHeaderName> keyAsHTTPHeaderName; String value; + + String asciiLowerCaseName() const + { + if (keyAsHTTPHeaderName) { + auto view = WTF::httpHeaderNameStringImpl(keyAsHTTPHeaderName.value()); + return String(view); + } + + return key.convertToASCIILowercase(); + } }; - const KeyValue* get() const + const KeyValue *get() const { ASSERT(*this != m_table.end()); return &m_keyValue; } - const KeyValue& operator*() const { return *get(); } - const KeyValue* operator->() const { return get(); } + const KeyValue &operator*() const { return *get(); } + const KeyValue *operator->() const { return get(); } - HTTPHeaderMapConstIterator& operator++() + HTTPHeaderMapConstIterator &operator++() { + if (m_commonHeadersIt != m_table.m_commonHeaders.end()) { if (updateKeyValue(++m_commonHeadersIt)) return *this; - } else + } else if (m_setCookiesIter != m_table.m_setCookieHeaders.end()) { + if (updateSetCookieHeaderPosition(++m_setCookiesIter)) + return *this; + } else { ++m_uncommonHeadersIt; + } updateKeyValue(m_uncommonHeadersIt); + return *this; } - bool operator!=(const HTTPHeaderMapConstIterator& other) const { return !(*this == other); } - bool operator==(const HTTPHeaderMapConstIterator& other) const + bool operator!=(const HTTPHeaderMapConstIterator &other) const { return !(*this == other); } + bool operator==(const HTTPHeaderMapConstIterator &other) const { - return m_commonHeadersIt == other.m_commonHeadersIt && m_uncommonHeadersIt == other.m_uncommonHeadersIt; + return m_commonHeadersIt == other.m_commonHeadersIt && m_uncommonHeadersIt == other.m_uncommonHeadersIt && m_setCookiesIter == other.m_setCookiesIter; } private: @@ -126,9 +146,22 @@ public: return true; } - const HTTPHeaderMap& m_table; + bool updateSetCookieHeaderPosition(Vector<String, 0>::const_iterator cookieI) + { + if (cookieI == m_table.m_setCookieHeaders.end()) { + return false; + } + + m_keyValue.key = httpHeaderNameString(HTTPHeaderName::SetCookie).toStringWithoutCopying(); + m_keyValue.keyAsHTTPHeaderName = HTTPHeaderName::SetCookie; + m_keyValue.value = *cookieI; + return true; + } + + const HTTPHeaderMap &m_table; CommonHeadersVector::const_iterator m_commonHeadersIt; UncommonHeadersVector::const_iterator m_uncommonHeadersIt; + Vector<String, 0>::const_iterator m_setCookiesIter; KeyValue m_keyValue; }; typedef HTTPHeaderMapConstIterator const_iterator; @@ -139,97 +172,112 @@ public: WEBCORE_EXPORT HTTPHeaderMap isolatedCopy() const &; WEBCORE_EXPORT HTTPHeaderMap isolatedCopy() &&; - bool isEmpty() const { return m_commonHeaders.isEmpty() && m_uncommonHeaders.isEmpty(); } - int size() const { return m_commonHeaders.size() + m_uncommonHeaders.size(); } + bool isEmpty() const { return m_commonHeaders.isEmpty() && m_uncommonHeaders.isEmpty() && m_setCookieHeaders.isEmpty(); } + int size() const { return m_commonHeaders.size() + m_uncommonHeaders.size() + m_setCookieHeaders.size(); } void clear() { m_commonHeaders.clear(); m_uncommonHeaders.clear(); + m_setCookieHeaders.clear(); } void shrinkToFit() { m_commonHeaders.shrinkToFit(); m_uncommonHeaders.shrinkToFit(); + m_setCookieHeaders.shrinkToFit(); } - WEBCORE_EXPORT String get(const String& name) const; - WEBCORE_EXPORT void set(const String& name, const String& value); - WEBCORE_EXPORT void add(const String& name, const String& value); - WEBCORE_EXPORT void append(const String& name, const String& value); - WEBCORE_EXPORT bool contains(const String&) const; - WEBCORE_EXPORT bool remove(const String&); + WEBCORE_EXPORT String get(const String &name) const; + WEBCORE_EXPORT void set(const String &name, const String &value); + WEBCORE_EXPORT void add(const String &name, const String &value); + WEBCORE_EXPORT void append(const String &name, const String &value); + WEBCORE_EXPORT bool contains(const String &) const; + WEBCORE_EXPORT bool remove(const String &); #if USE(CF) - void set(CFStringRef name, const String& value); + void set(CFStringRef name, const String &value); #ifdef __OBJC__ - void set(NSString *name, const String& value) { set((__bridge CFStringRef)name, value); } + void set(NSString *name, const String &value) + { + set((__bridge CFStringRef)name, value); + } #endif #endif WEBCORE_EXPORT String get(HTTPHeaderName) const; - void set(HTTPHeaderName, const String& value); - void add(HTTPHeaderName, const String& value); - bool addIfNotPresent(HTTPHeaderName, const String&); + void set(HTTPHeaderName, const String &value); + void add(HTTPHeaderName, const String &value); + bool addIfNotPresent(HTTPHeaderName, const String &); WEBCORE_EXPORT bool contains(HTTPHeaderName) const; WEBCORE_EXPORT bool remove(HTTPHeaderName); // Instead of passing a string literal to any of these functions, just use a HTTPHeaderName instead. template<size_t length> String get(const char (&)[length]) const = delete; - template<size_t length> void set(const char (&)[length], const String&) = delete; + template<size_t length> void set(const char (&)[length], const String &) = delete; template<size_t length> bool contains(const char (&)[length]) = delete; template<size_t length> bool remove(const char (&)[length]) = delete; - const CommonHeadersVector& commonHeaders() const { return m_commonHeaders; } - const UncommonHeadersVector& uncommonHeaders() const { return m_uncommonHeaders; } - CommonHeadersVector& commonHeaders() { return m_commonHeaders; } - UncommonHeadersVector& uncommonHeaders() { return m_uncommonHeaders; } + const Vector<String, 0> &getSetCookieHeaders() const { return m_setCookieHeaders; } + const CommonHeadersVector &commonHeaders() const { return m_commonHeaders; } + const UncommonHeadersVector &uncommonHeaders() const { return m_uncommonHeaders; } + CommonHeadersVector &commonHeaders() { return m_commonHeaders; } + UncommonHeadersVector &uncommonHeaders() { return m_uncommonHeaders; } - const_iterator begin() const { return const_iterator(*this, m_commonHeaders.begin(), m_uncommonHeaders.begin()); } - const_iterator end() const { return const_iterator(*this, m_commonHeaders.end(), m_uncommonHeaders.end()); } + const_iterator begin() const { return const_iterator(*this, m_commonHeaders.begin(), m_uncommonHeaders.begin(), m_setCookieHeaders.begin()); } + const_iterator end() const { return const_iterator(*this, m_commonHeaders.end(), m_uncommonHeaders.end(), m_setCookieHeaders.end()); } - friend bool operator==(const HTTPHeaderMap& a, const HTTPHeaderMap& b) + friend bool operator==(const HTTPHeaderMap &a, const HTTPHeaderMap &b) { - if (a.m_commonHeaders.size() != b.m_commonHeaders.size() || a.m_uncommonHeaders.size() != b.m_uncommonHeaders.size()) + if (a.m_commonHeaders.size() != b.m_commonHeaders.size() || a.m_uncommonHeaders.size() != b.m_uncommonHeaders.size() || a.m_setCookieHeaders.size() != b.m_setCookieHeaders.size()) return false; - for (auto& commonHeader : a.m_commonHeaders) { + + for (auto &commonHeader : a.m_commonHeaders) { if (b.get(commonHeader.key) != commonHeader.value) return false; } - for (auto& uncommonHeader : a.m_uncommonHeaders) { + + for (auto &uncommonHeader : a.m_setCookieHeaders) { + if (b.m_setCookieHeaders.find(uncommonHeader) == notFound) + return false; + } + + for (auto &uncommonHeader : a.m_uncommonHeaders) { if (b.getUncommonHeader(uncommonHeader.key) != uncommonHeader.value) return false; } + return true; } - friend bool operator!=(const HTTPHeaderMap& a, const HTTPHeaderMap& b) + friend bool operator!=(const HTTPHeaderMap &a, const HTTPHeaderMap &b) { return !(a == b); } - template <class Encoder> void encode(Encoder&) const; - template <class Decoder> static WARN_UNUSED_RETURN bool decode(Decoder&, HTTPHeaderMap&); - void setUncommonHeader(const String& name, const String& value); - void setUncommonHeaderCloneName(const StringView name, const String& value); + template<class Encoder> void encode(Encoder &) const; + template<class Decoder> static WARN_UNUSED_RETURN bool decode(Decoder &, HTTPHeaderMap &); + void setUncommonHeader(const String &name, const String &value); + void setUncommonHeaderCloneName(const StringView name, const String &value); private: - WEBCORE_EXPORT String getUncommonHeader(const String& name) const; + WEBCORE_EXPORT String getUncommonHeader(const String &name) const; CommonHeadersVector m_commonHeaders; UncommonHeadersVector m_uncommonHeaders; + Vector<String, 0> m_setCookieHeaders; }; -template <class Encoder> -void HTTPHeaderMap::CommonHeader::encode(Encoder& encoder) const +template<class Encoder> +void HTTPHeaderMap::CommonHeader::encode(Encoder &encoder) const { encoder << key; encoder << value; } -template <class Decoder> -auto HTTPHeaderMap::CommonHeader::decode(Decoder& decoder) -> std::optional<CommonHeader> +template<class Decoder> +auto HTTPHeaderMap::CommonHeader::decode(Decoder &decoder) -> std::optional<CommonHeader> { HTTPHeaderName name; if (!decoder.decode(name)) @@ -241,15 +289,15 @@ auto HTTPHeaderMap::CommonHeader::decode(Decoder& decoder) -> std::optional<Comm return CommonHeader { name, WTFMove(value) }; } -template <class Encoder> -void HTTPHeaderMap::UncommonHeader::encode(Encoder& encoder) const +template<class Encoder> +void HTTPHeaderMap::UncommonHeader::encode(Encoder &encoder) const { encoder << key; encoder << value; } -template <class Decoder> -auto HTTPHeaderMap::UncommonHeader::decode(Decoder& decoder) -> std::optional<UncommonHeader> +template<class Decoder> +auto HTTPHeaderMap::UncommonHeader::decode(Decoder &decoder) -> std::optional<UncommonHeader> { String name; if (!decoder.decode(name)) @@ -261,15 +309,15 @@ auto HTTPHeaderMap::UncommonHeader::decode(Decoder& decoder) -> std::optional<Un return UncommonHeader { WTFMove(name), WTFMove(value) }; } -template <class Encoder> -void HTTPHeaderMap::encode(Encoder& encoder) const +template<class Encoder> +void HTTPHeaderMap::encode(Encoder &encoder) const { encoder << m_commonHeaders; encoder << m_uncommonHeaders; } -template <class Decoder> -bool HTTPHeaderMap::decode(Decoder& decoder, HTTPHeaderMap& headerMap) +template<class Decoder> +bool HTTPHeaderMap::decode(Decoder &decoder, HTTPHeaderMap &headerMap) { if (!decoder.decode(headerMap.m_commonHeaders)) return false; diff --git a/src/bun.js/bindings/webcore/HTTPHeaderNames.h b/src/bun.js/bindings/webcore/HTTPHeaderNames.h index 246afeefa..870c4980f 100644 --- a/src/bun.js/bindings/webcore/HTTPHeaderNames.h +++ b/src/bun.js/bindings/webcore/HTTPHeaderNames.h @@ -334,12 +334,11 @@ static StaticStringImpl* staticHeaderNames[] = { MAKE_STATIC_STRING_IMPL("x-xss-protection"), }; -static WTF::StaticStringImpl* httpHeaderNameStringImpl(WebCore::HTTPHeaderName headerName) { +static WTF::StaticStringImpl* httpHeaderNameStringImpl(WebCore::HTTPHeaderName headerName) +{ return staticHeaderNames[static_cast<size_t>(headerName)]; } - } // namespace WTF - #endif // HTTPHeaderNames_h diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp index 2c83778a2..94b574983 100644 --- a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp +++ b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp @@ -56,6 +56,8 @@ #include <wtf/URL.h> #include <wtf/Vector.h> +#include "GCDefferalContext.h" + namespace WebCore { using namespace JSC; @@ -182,6 +184,107 @@ JSC_DEFINE_CUSTOM_GETTER(jsFetchHeadersGetterCount, (JSC::JSGlobalObject * globa return JSValue::encode(jsNumber(count)); } +JSC_DEFINE_HOST_FUNCTION(jsFetchHeadersPrototypeFunction_getAll, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSFetchHeaders* castedThis = jsDynamicCast<JSFetchHeaders*>(callFrame->thisValue()); + if (UNLIKELY(!castedThis)) { + return JSValue::encode(jsUndefined()); + } + + if (UNLIKELY(!callFrame->argumentCount())) { + throwTypeError(lexicalGlobalObject, scope, "Missing argument"_s); + return JSValue::encode(jsUndefined()); + } + + auto name = convert<IDLByteString>(*lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + + auto& impl = castedThis->wrapped(); + if (name.length() != "set-cookie"_s.length() || name.convertToASCIILowercase() != "set-cookie"_s) { + return JSValue::encode(JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0)); + } + + auto values = impl.getSetCookieHeaders(); + unsigned count = values.size(); + if (!count) { + return JSValue::encode(JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0)); + } + + JSC::JSArray* array = nullptr; + GCDeferralContext deferralContext(lexicalGlobalObject->vm()); + JSC::ObjectInitializationScope initializationScope(lexicalGlobalObject->vm()); + if ((array = JSC::JSArray::tryCreateUninitializedRestricted( + initializationScope, &deferralContext, + lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + count))) { + for (unsigned i = 0; i < count; ++i) { + array->initializeIndex(initializationScope, i, jsString(vm, values[i])); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + } else { + array = constructEmptyArray(lexicalGlobalObject, nullptr, count); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + if (!array) { + throwOutOfMemoryError(lexicalGlobalObject, scope); + return JSValue::encode(jsUndefined()); + } + for (unsigned i = 0; i < count; ++i) { + array->putDirectIndex(lexicalGlobalObject, i, jsString(vm, values[i])); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + + return JSValue::encode(array); +} + +JSC_DEFINE_HOST_FUNCTION(jsFetchHeadersPrototypeFunction_getSetCookie, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSFetchHeaders* castedThis = jsDynamicCast<JSFetchHeaders*>(callFrame->thisValue()); + if (UNLIKELY(!castedThis)) { + return JSValue::encode(jsUndefined()); + } + + auto& impl = castedThis->wrapped(); + auto values = impl.getSetCookieHeaders(); + unsigned count = values.size(); + + if (!count) { + return JSValue::encode(JSC::constructEmptyArray(lexicalGlobalObject, nullptr, 0)); + } + + JSC::JSArray* array = nullptr; + GCDeferralContext deferralContext(lexicalGlobalObject->vm()); + JSC::ObjectInitializationScope initializationScope(lexicalGlobalObject->vm()); + if ((array = JSC::JSArray::tryCreateUninitializedRestricted( + initializationScope, &deferralContext, + lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + count))) { + for (unsigned i = 0; i < count; ++i) { + array->initializeIndex(initializationScope, i, jsString(vm, values[i])); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + } else { + array = constructEmptyArray(lexicalGlobalObject, nullptr, count); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + if (!array) { + throwOutOfMemoryError(lexicalGlobalObject, scope); + return JSValue::encode(jsUndefined()); + } + for (unsigned i = 0; i < count; ++i) { + array->putDirectIndex(lexicalGlobalObject, i, jsString(vm, values[i])); + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); + } + + return JSValue::encode(array); +} + /* Hash table for prototype */ static const HashTableValue JSFetchHeadersPrototypeTableValues[] = { @@ -189,6 +292,7 @@ static const HashTableValue JSFetchHeadersPrototypeTableValues[] = { { "append"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_append, 2 } }, { "delete"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_delete, 1 } }, { "get"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_get, 1 } }, + { "getAll"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_getAll, 1 } }, { "has"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_has, 1 } }, { "set"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_set, 2 } }, { "entries"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_entries, 0 } }, @@ -197,6 +301,7 @@ static const HashTableValue JSFetchHeadersPrototypeTableValues[] = { { "forEach"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_forEach, 1 } }, { "toJSON"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_toJSON, 0 } }, { "count"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsFetchHeadersGetterCount, 0 } }, + { "getSetCookie"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_getSetCookie, 0 } }, }; const ClassInfo JSFetchHeadersPrototype::s_info = { "Headers"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSFetchHeadersPrototype) }; @@ -305,6 +410,41 @@ static inline JSC::EncodedJSValue jsFetchHeadersPrototypeFunction_toJSONBody(JSC } { + auto& values = internal.getSetCookieHeaders(); + + size_t count = values.size(); + + if (count > 0) { + JSC::JSArray* array = nullptr; + GCDeferralContext deferralContext(lexicalGlobalObject->vm()); + JSC::ObjectInitializationScope initializationScope(lexicalGlobalObject->vm()); + if ((array = JSC::JSArray::tryCreateUninitializedRestricted( + initializationScope, &deferralContext, + lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), + count))) { + for (unsigned i = 0; i < count; ++i) { + array->initializeIndex(initializationScope, i, jsString(vm, values[i])); + RETURN_IF_EXCEPTION(throwScope, JSValue::encode(jsUndefined())); + } + } else { + array = constructEmptyArray(lexicalGlobalObject, nullptr, count); + RETURN_IF_EXCEPTION(throwScope, JSValue::encode(jsUndefined())); + if (!array) { + throwOutOfMemoryError(lexicalGlobalObject, throwScope); + return JSValue::encode(jsUndefined()); + } + for (unsigned i = 0; i < count; ++i) { + array->putDirectIndex(lexicalGlobalObject, i, jsString(vm, values[i])); + RETURN_IF_EXCEPTION(throwScope, JSValue::encode(jsUndefined())); + } + RETURN_IF_EXCEPTION(throwScope, JSValue::encode(jsUndefined())); + } + + obj->putDirect(vm, JSC::Identifier::fromString(vm, httpHeaderNameString(HTTPHeaderName::SetCookie).toStringWithoutCopying()), array, 0); + } + } + + { auto& vec = internal.uncommonHeaders(); for (auto it = vec.begin(); it != vec.end(); ++it) { auto& name = it->key; diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.h b/src/bun.js/bindings/webcore/JSFetchHeaders.h index 4761e3a66..74fe24601 100644 --- a/src/bun.js/bindings/webcore/JSFetchHeaders.h +++ b/src/bun.js/bindings/webcore/JSFetchHeaders.h @@ -57,6 +57,7 @@ public: } static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); + protected: JSFetchHeaders(JSC::Structure*, JSDOMGlobalObject&, Ref<FetchHeaders>&&); diff --git a/test/bun.js/serve.test.ts b/test/bun.js/serve.test.ts index 91b57a738..6e5a0030a 100644 --- a/test/bun.js/serve.test.ts +++ b/test/bun.js/serve.test.ts @@ -599,6 +599,31 @@ it("should support reloading", async () => { server.stop(); }); +it("should support multiple Set-Cookie headers", async () => { + const server = serve({ + port: port++, + fetch(req) { + return new Response("hello", { + headers: [ + ["Another-Header", "1"], + ["Set-Cookie", "foo=bar"], + ["Set-Cookie", "baz=qux"], + ], + }); + }, + }); + + const response = await fetch(`http://${server.hostname}:${server.port}`); + server.stop(); + + expect(response.headers.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); + expect(response.headers.getSetCookie()).toEqual(["foo=bar", "baz=qux"]); + + const cloned = response.clone().headers; + expect(cloned.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); + expect(cloned.getSetCookie()).toEqual(["foo=bar", "baz=qux"]); +}); + describe("status code text", () => { const fixture = { 200: "OK", |