Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/jsc/FetchHeaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ bun_opaque::opaque_ffi! {
pub struct FetchHeaders;
}

/// Mirrors `HeadersInitName` in headers-handwritten.h.
#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum HeadersInitName {
/// `headers`
Headers = 0,
/// `proxy.headers`
ProxyHeaders = 1,
}

// `FetchHeaders`/`JSGlobalObject`/`VM` are opaque `UnsafeCell`-backed ZST
// handles, so `&T` is ABI-identical to a non-null `*const T` and C++ mutating
// header storage / VM state through them is interior mutation invisible to
Expand Down Expand Up @@ -74,6 +84,7 @@ unsafe extern "C" {
safe fn WebCore__FetchHeaders__createFromJS(
arg0: &JSGlobalObject,
arg1: JSValue,
name: HeadersInitName,
) -> *mut FetchHeaders;

safe fn WebCore__FetchHeaders__put(
Expand Down Expand Up @@ -101,15 +112,24 @@ impl FetchHeaders {
/// - Array<[String, String]>
/// - Record<String, String>.
///
/// Throws an exception if invalid.
/// Throws an exception if invalid. A wrong type names the `headers` option.
///
/// If empty, returns null.
pub fn create_from_js(
global: &JSGlobalObject,
value: JSValue,
) -> JsResult<Option<NonNull<FetchHeaders>>> {
Self::create_from_js_named(global, value, HeadersInitName::Headers)
}

/// [`Self::create_from_js`] for a value that a different option held.
pub fn create_from_js_named(
global: &JSGlobalObject,
value: JSValue,
name: HeadersInitName,
) -> JsResult<Option<NonNull<FetchHeaders>>> {
host_fn::from_js_host_call_generic(global, || {
NonNull::new(WebCore__FetchHeaders__createFromJS(global, value))
NonNull::new(WebCore__FetchHeaders__createFromJS(global, value, name))
})
}

Expand Down
9 changes: 4 additions & 5 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2337,7 +2337,7 @@ WebCore::FetchHeaders* WebCore__FetchHeaders__cast_(JSC::EncodedJSValue JSValue0
return WebCoreCast<WebCore::JSFetchHeaders, WebCore::FetchHeaders>(JSValue0);
}

WebCore::FetchHeaders* WebCore__FetchHeaders__createFromJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue argument0_)
WebCore::FetchHeaders* WebCore__FetchHeaders__createFromJS(JSC::JSGlobalObject* lexicalGlobalObject, JSC::EncodedJSValue argument0_, HeadersInitName name)
{
EnsureStillAliveScope argument0 = JSC::JSValue::decode(argument0_);

Expand All @@ -2347,10 +2347,9 @@ WebCore::FetchHeaders* WebCore__FetchHeaders__createFromJS(JSC::JSGlobalObject*
// Note that we use IDLDOMString here rather than IDLByteString: while headers
// should be ASCII only, we want the headers->fill implementation to discover
// and error on invalid names and values
using TargetType = IDLUnion<IDLSequence<IDLSequence<IDLDOMString>>, IDLRecord<IDLDOMString, IDLDOMString>>;
using Converter = std::optional<Converter<TargetType>::ReturnType>;

auto init = argument0.value().isUndefined() ? Converter() : Converter(convert<TargetType>(*lexicalGlobalObject, argument0.value()));
std::optional<WebCore::FetchHeaders::Init> init;
if (!argument0.value().isUndefined())
init = convertHeadersInit<IDLDOMString>(*lexicalGlobalObject, argument0.value(), name);
RETURN_IF_EXCEPTION(throwScope, nullptr);

// if the headers are empty, return null
Expand Down
6 changes: 6 additions & 0 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ enum class UWSResponseKind : int32_t {
H2 = 2,
H3 = 3,
};

/// Mirrors `HeadersInitName` in src/jsc/FetchHeaders.rs.
enum class HeadersInitName : uint8_t {
Headers = 0,
ProxyHeaders = 1,
};
#endif

typedef struct BunString {
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/headers.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 72 additions & 2 deletions src/jsc/bindings/webcore/JSFetchHeaders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "config.h"
#include "JSFetchHeaders.h"

#include "ErrorCode.h"
#include "ExtendedDOMClientIsoSubspaces.h"
#include "ExtendedDOMIsoSubspaces.h"
#include "IDLTypes.h"
Expand Down Expand Up @@ -122,6 +123,75 @@ size_t JSFetchHeaders::estimatedSize(JSC::JSCell* cell, JSC::VM& vm)
return Base::estimatedSize(cell, vm) + wrapped.memoryCost();
}

static void throwEntryIsNotPair(JSGlobalObject& lexicalGlobalObject, ThrowScope& scope, ASCIILiteral name, size_t index, JSValue entry)
{
auto entryName = makeString(name, '[', index, ']');
// INVALID_ARG_INSTANCE describes an object by its `constructor.name`, which runs user code.
if (!entry.isObject()) {
Bun::ERR::INVALID_ARG_INSTANCE(scope, &lexicalGlobalObject, entryName, "Array"_s, entry);
return;
}
auto kind = StringView(name).contains('.') ? "property"_s : "argument"_s;
Bun::throwError(&lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, makeString("The \""_s, entryName, "\" "_s, kind, " must be an instance of Array"_s));
}

template<typename IDLStringType>
static FetchHeaders::Init convertHeadersInitNamed(JSGlobalObject& lexicalGlobalObject, JSValue value, ASCIILiteral name)
{
auto& vm = JSC::getVM(&lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

auto* object = value.getObject();
if (!object) {
Bun::ERR::INVALID_ARG_INSTANCE(scope, &lexicalGlobalObject, name, "Headers, Array, or Object"_s, value);
return {};
}

auto method = JSC::iteratorMethod(&lexicalGlobalObject, object);
RETURN_IF_EXCEPTION(scope, {});
if (method.isUndefined())
RELEASE_AND_RETURN(scope, (Converter<IDLRecord<IDLStringType, IDLStringType>>::convert(lexicalGlobalObject, value)));

Vector<Vector<String>> pairs;
forEachInIterable(lexicalGlobalObject, object, method, [&pairs, name](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) {
auto scope = DECLARE_THROW_SCOPE(vm);

Vector<String> pair;
if (JSC::getIterationMode(nextValue) == JSC::IterationMode::FastArray) {
// No Symbol.iterator lookup: it is 13% of `new Headers()` for 8 pairs.
pair = Converter<IDLSequence<IDLStringType>>::convert(lexicalGlobalObject, nextValue);
} else {
auto* entry = nextValue.getObject();
auto entryMethod = entry ? JSC::iteratorMethod(&lexicalGlobalObject, entry) : jsUndefined();
RETURN_IF_EXCEPTION(scope, );
if (entryMethod.isUndefined()) {
throwEntryIsNotPair(lexicalGlobalObject, scope, name, pairs.size(), nextValue);
return;
}
pair = Converter<IDLSequence<IDLStringType>>::convert(lexicalGlobalObject, entry, entryMethod);
}
RETURN_IF_EXCEPTION(scope, );
pairs.append(WTF::move(pair));
});
RETURN_IF_EXCEPTION(scope, {});
return pairs;
}

template<typename IDLStringType>
FetchHeaders::Init convertHeadersInit(JSGlobalObject& lexicalGlobalObject, JSValue value, HeadersInitName name)
{
switch (name) {
case HeadersInitName::Headers:
return convertHeadersInitNamed<IDLStringType>(lexicalGlobalObject, value, "headers"_s);
case HeadersInitName::ProxyHeaders:
return convertHeadersInitNamed<IDLStringType>(lexicalGlobalObject, value, "proxy.headers"_s);
}
RELEASE_ASSERT_NOT_REACHED();
}

template FetchHeaders::Init convertHeadersInit<IDLDOMString>(JSGlobalObject&, JSValue, HeadersInitName);
template FetchHeaders::Init convertHeadersInit<IDLByteString>(JSGlobalObject&, JSValue, HeadersInitName);

template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSFetchHeadersDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame)
{
auto& vm = JSC::getVM(lexicalGlobalObject);
Expand All @@ -130,7 +200,7 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSFetchHeadersDOMConstru
ASSERT(castedThis);
EnsureStillAliveScope argument0 = callFrame->argument(0);

auto init = std::optional<Converter<IDLUnion<IDLSequence<IDLSequence<IDLDOMString>>, IDLRecord<IDLDOMString, IDLDOMString>>>::ReturnType>();
std::optional<FetchHeaders::Init> init;

if (argument0.value() && !argument0.value().isUndefined()) {
if (auto* existingJsFetchHeaders = dynamicDowncast<JSFetchHeaders>(argument0.value())) {
Expand All @@ -142,7 +212,7 @@ template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSFetchHeadersDOMConstru
RETURN_IF_EXCEPTION(throwScope, {});
return JSValue::encode(jsValue);
}
init = std::optional<Converter<IDLUnion<IDLSequence<IDLSequence<IDLDOMString>>, IDLRecord<IDLDOMString, IDLDOMString>>>::ReturnType>(convert<IDLUnion<IDLSequence<IDLSequence<IDLDOMString>>, IDLRecord<IDLDOMString, IDLDOMString>>>(*lexicalGlobalObject, argument0.value()));
init = convertHeadersInitNamed<IDLDOMString>(*lexicalGlobalObject, argument0.value(), "init"_s);
}

RETURN_IF_EXCEPTION(throwScope, {});
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/webcore/JSFetchHeaders.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include "FetchHeaders.h"
#include "JSDOMWrapper.h"
#include "headers-handwritten.h"

namespace WebCore {

Expand Down Expand Up @@ -82,4 +83,8 @@ template<> struct JSDOMWrapperConverterTraits<FetchHeaders> {

JSC::EncodedJSValue fetchHeadersGetSetCookie(JSC::JSGlobalObject* lexicalGlobalObject, VM& vm, WebCore::FetchHeaders* impl);

// The IDLUnion steps for a HeadersInit. A TypeError from the conversion itself names the option.
template<typename IDLStringType>
FetchHeaders::Init convertHeadersInit(JSC::JSGlobalObject&, JSC::JSValue, HeadersInitName);

} // namespace WebCore
8 changes: 4 additions & 4 deletions src/jsc/bindings/webcore/JSWebSocket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,22 +221,22 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
// Native heap SSLConfig. RAII — freed on any early return, moved into
// WebSocket::create() on success.
WebSocketSSLConfigPtr sslConfig;
auto headersInit = std::optional<Converter<IDLUnion<IDLSequence<IDLSequence<IDLByteString>>, IDLRecord<IDLByteString, IDLByteString>>>::ReturnType>();
std::optional<FetchHeaders::Init> headersInit;
// Default true — matches Bun's existing behavior of always offering permessage-deflate.
// ws.WebSocket passes `perMessageDeflate: false` to opt out.
bool offerPerMessageDeflate = true;

// Proxy options
String proxyUrl;
auto proxyHeadersInit = std::optional<Converter<IDLUnion<IDLSequence<IDLSequence<IDLByteString>>, IDLRecord<IDLByteString, IDLByteString>>>::ReturnType>();
std::optional<FetchHeaders::Init> proxyHeadersInit;

if (JSC::JSObject* options = optionsObjectValue.getObject()) {
const auto& builtinnames = WebCore::builtinNames(vm);
auto headersValue = Bun::getOwnPropertyIfExists(globalObject, options, builtinnames.headersPublicName());
RETURN_IF_EXCEPTION(throwScope, {});
if (headersValue) {
if (!headersValue.isUndefinedOrNull()) {
headersInit = convert<IDLUnion<IDLSequence<IDLSequence<IDLByteString>>, IDLRecord<IDLByteString, IDLByteString>>>(*lexicalGlobalObject, headersValue);
headersInit = convertHeadersInit<IDLByteString>(*lexicalGlobalObject, headersValue, HeadersInitName::Headers);
RETURN_IF_EXCEPTION(throwScope, {});
}
}
Expand Down Expand Up @@ -324,7 +324,7 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG
proxyHeadersInit = WTF::move(pairs);
} else {
// Fall back to IDL conversion for plain objects/arrays
proxyHeadersInit = convert<IDLUnion<IDLSequence<IDLSequence<IDLByteString>>, IDLRecord<IDLByteString, IDLByteString>>>(*lexicalGlobalObject, proxyHeadersValue);
proxyHeadersInit = convertHeadersInit<IDLByteString>(*lexicalGlobalObject, proxyHeadersValue, HeadersInitName::ProxyHeaders);
RETURN_IF_EXCEPTION(throwScope, {});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ pub use self::resolved_source_tag::ResolvedSourceTag;
// ──────────────────────────────────────────────────────────────────────────
#[path = "FetchHeaders.rs"]
pub mod fetch_headers;
pub use self::fetch_headers::{FetchHeaders, HTTPHeaderName};
pub use self::fetch_headers::{FetchHeaders, HTTPHeaderName, HeadersInitName};

/// `BuiltinName` — fast-path property keys preallocated as `JSC::Identifier`s
/// in C++ (`BunBuiltinNames.h`). Passed to `JSValue::fast_get` as a `u8` index
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/webcore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ bun_collections::object_pool!(pub ByteListPool: Vec<u8>, threadsafe, 8);
// Re-export the crate-local jsc shim's opaque type until `bun_jsc::fetch_headers`
// is green; the shim's `#[repr(transparent)] struct FetchHeaders(usize)` matches the
// opaque-handle ABI used by the `WebCore__FetchHeaders__*` extern fns.
pub use crate::jsc::FetchHeaders;
pub use crate::jsc::{FetchHeaders, HeadersInitName};

#[path = "webcore/EncodingLabel.rs"]
pub mod encoding_label;
Expand Down
15 changes: 13 additions & 2 deletions src/runtime/webcore/Response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use bun_core::{String as BunString, Utf8Bytes};
use bun_http_types::Method::Method;

use super::body::{Body, BodyMixin, Value as BodyValue, ValueError as BodyValueError};
use super::{FetchHeaders, ReadableStream, Request};
use super::{FetchHeaders, HeadersInitName, ReadableStream, Request};

// Codegen (`generated_classes.rs`) re-exports `Blob` from
// `crate::webcore::response` because the `.classes.ts` source path is
Expand Down Expand Up @@ -71,9 +71,20 @@ impl HeadersRef {
pub(crate) fn create_from_js(
global: &JSGlobalObject,
value: JSValue,
) -> JsResult<Option<Self>> {
Self::create_from_js_named(global, value, HeadersInitName::Headers)
}

/// [`Self::create_from_js`] for a value that a different option held.
#[inline]
pub(crate) fn create_from_js_named(
global: &JSGlobalObject,
value: JSValue,
name: HeadersInitName,
) -> JsResult<Option<Self>> {
// SAFETY: C++ returns a +1 ref or null.
Ok(FetchHeaders::create_from_js(global, value)?.map(|p| unsafe { Self::adopt(p) }))
Ok(FetchHeaders::create_from_js_named(global, value, name)?
.map(|p| unsafe { Self::adopt(p) }))
}

/// `FetchHeaders.cloneThis(global)` — deep copy on the C++ side.
Expand Down
8 changes: 6 additions & 2 deletions src/runtime/webcore/fetch/FetchSession.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use bun_jsc::{
};

use crate::socket::ssl_config::{SSLConfig, SSLConfigFromJs as _};
use crate::webcore::FetchHeaders;
use crate::webcore::response::HeadersRef;
use crate::webcore::{FetchHeaders, HeadersInitName};

pub use crate::generated_classes::js_FetchSession as js;

Expand Down Expand Up @@ -124,7 +124,11 @@ pub(crate) fn parse_proxy(
if let Some(fetch_headers) = FetchHeaders::cast(headers_value) {
let fetch_headers = bun_ptr::BackRef::from(fetch_headers);
headers = Some(from_fetch_headers(Some(&*fetch_headers), None));
} else if let Some(fetch_headers) = HeadersRef::create_from_js(global, headers_value)? {
} else if let Some(fetch_headers) = HeadersRef::create_from_js_named(
global,
headers_value,
HeadersInitName::ProxyHeaders,
)? {
headers = Some(from_fetch_headers(Some(&fetch_headers), None));
}
}
Expand Down
6 changes: 6 additions & 0 deletions test/js/web/fetch/fetch-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,12 @@ describe("Bun.FetchSession", () => {
expect(construct({ proxy: { url: "http://p", respectNoProxy: 1 } })).toThrow(
'The "respectNoProxy" property must be of type boolean',
);
expect(construct({ proxy: { url: "http://p", headers: 1 } })).toThrow(
'The "proxy.headers" property must be an instance of Headers, Array, or Object. Received type number (1)',
);
expect(construct({ proxy: { url: "http://p", headers: ["a"] } })).toThrow(
`The "proxy.headers[0]" property must be an instance of Array. Received type string ('a')`,
);
expect(construct({ unix: "/tmp/x.sock", proxy: "http://p" })).toThrow(
"FetchSession: cannot use a proxy with a unix socket",
);
Expand Down
Loading
Loading