mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
[fetch] Implement Headers#getAll and Headers#getSetCookie()
This matches Deno's behavior (get() combines, iterator preserves the order, set and append combine), but implements both the Cloudflare Workers `getAll()` and the potential standard `getSetCookie` function. The rationale for choosing both is to better support libraries which check for `getAll` and also because `getSetCookie` seems a little confusing (names are hard) This also makes `.toJSON` and JSON.stringify return an array for `Set-Cookie`
This commit is contained in:
36
packages/bun-types/globals.d.ts
vendored
36
packages/bun-types/globals.d.ts
vendored
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) };
|
||||
@@ -304,6 +409,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) {
|
||||
|
||||
@@ -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>&&);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user