Skip to content
Draft
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
16 changes: 16 additions & 0 deletions src/jsc/JSGlobalObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,17 @@ impl JSGlobalObject {
})
}

/// A promise that rejects with `error` once the stream has been read to its end.
pub fn readable_stream_consume_then_reject(
&self,
value: JSValue,
error: JSValue,
) -> JsResult<JSValue> {
crate::call_zero_is_throw(self, || {
ZigGlobalObject__readableStreamConsumeThenReject(self, value, error)
})
}

/// Returns a freshly-created `napi_env` owned by this global, for use by
/// the FFI module. The concrete `NapiEnv` struct lives in `bun_runtime`
/// (which depends on `bun_jsc`), so this returns the raw pointer untyped;
Expand Down Expand Up @@ -1591,6 +1602,11 @@ unsafe extern "C" {
value: JSValue,
content_type: JSValue,
) -> JSValue;
safe fn ZigGlobalObject__readableStreamConsumeThenReject(
this: &JSGlobalObject,
value: JSValue,
error: JSValue,
) -> JSValue;
safe fn ZigGlobalObject__readableStreamToBlob(this: &JSGlobalObject, value: JSValue)
-> JSValue;

Expand Down
23 changes: 21 additions & 2 deletions src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1311,7 +1311,8 @@ JSValue readableStreamToBlob(JSGlobalObject* globalObject, WebCore::JSReadableSt
return derived;
}

JSValue readableStreamToFormData(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, JSValue contentType)
// Reads `stream` to its end as a Blob; `(runtime->*handler)()(blob, context)` then settles the result.
static JSValue readableStreamToBlobThen(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, JSFunction* (JSStreamsRuntime::*handler)() const, JSValue context)
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
Expand All @@ -1326,10 +1327,20 @@ JSValue readableStreamToFormData(JSGlobalObject* globalObject, WebCore::JSReadab
}
auto* runtime = JSStreamsRuntime::from(globalObject);
auto* derived = JSPromise::create(vm, globalObject->promiseStructure());
blobPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToFormDataFulfilled(), jsUndefined(), derived, contentType);
blobPromise->performPromiseThenWithContext(vm, globalObject, (runtime->*handler)(), jsUndefined(), derived, context);
return derived;
}

JSValue readableStreamToFormData(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, JSValue contentType)
{
return readableStreamToBlobThen(globalObject, stream, &JSStreamsRuntime::onReadableStreamToFormDataFulfilled, contentType);
}

JSValue readableStreamConsumeThenReject(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, JSValue error)
{
return readableStreamToBlobThen(globalObject, stream, &JSStreamsRuntime::onReadableStreamConsumedThenReject, error);
}

} // namespace WebStreams
} // namespace Bun

Expand Down Expand Up @@ -1534,6 +1545,14 @@ JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToFormDataFulfilled
RELEASE_AND_RETURN(scope, JSValue::encode(JSC::call(globalObject, fromFunction, callData, constructor, arguments)));
}

JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamConsumedThenReject, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
throwException(globalObject, scope, callFrame->argument(1));
return {};
}

JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onIntoArrayReadManyFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = getVM(globalObject);
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/webcore/streams/JSStreamsRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ namespace WebCore {
// onReadableStreamTo*Fulfilled: the generic-path promise chains
// (toArrayBuffer/toBytes/toBlob: value = the chunk array; toJSON: value = the text;
// toFormData: value = the Blob, context = the contentType JSString).
// onReadableStreamConsumedThenReject: value = the Blob (dropped), context = the error to throw.
// onIntoArrayReadMany*: readableStreamIntoArray's readMany() continuation (readMany may
// return a Promise); context = an InternalFieldTuple{reader, resultArray}.
// onDirectConsumeLoopRead*: the readableStreamTo{Text,Array}Direct read loop;
Expand All @@ -218,6 +219,7 @@ namespace WebCore {
V(onReadableStreamToJSONFulfilled) \
V(onReadableStreamToBlobFulfilled) \
V(onReadableStreamToFormDataFulfilled) \
V(onReadableStreamConsumedThenReject) \
V(onIntoArrayReadManyFulfilled) /* append value; !done => readMany() again; done => release + resolve */ \
V(onIntoArrayReadManyRejected) /* release the reader, reject the result promise */ \
V(onIntoArrayReadFulfilled) /* persistent-op pump: append the read chunk, keep filling */ \
Expand Down
9 changes: 9 additions & 0 deletions src/jsc/bindings/webcore/streams/WebStreamsExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,12 @@ extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::Gl
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToFormData(globalObject, stream, JSValue::decode(contentType))));
}

extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamConsumeThenReject(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue, JSC::EncodedJSValue error)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stream = toReadableStream(globalObject, scope, streamValue);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamConsumeThenReject(globalObject, stream, JSValue::decode(error))));
}
3 changes: 3 additions & 0 deletions src/jsc/bindings/webcore/streams/WebStreamsInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,8 @@ JSC::JSValue readableStreamToBytes(JSC::JSGlobalObject*, JSReadableStream*); //
JSC::JSValue readableStreamToJSON(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp
JSC::JSValue readableStreamToBlob(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp
JSC::JSValue readableStreamToFormData(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue contentType); // userJS: yes — BunStreamConsumers.cpp
// Body.formData() on a body that is not form data: the stream is read to its end like any body, then `error` rejects the result.
JSC::JSValue readableStreamConsumeThenReject(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue error); // userJS: yes — BunStreamConsumers.cpp

// The buffered fast path: returns the native handle's own .text()/.arrayBuffer()/... promise,
// or the EMPTY JSValue if the fast path does not apply. `method` is the property name to
Expand Down Expand Up @@ -735,5 +737,6 @@ JSC::EncodedJSValue ZigGlobalObject__readableStreamToText(Zig::GlobalObject*, JS
JSC::EncodedJSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes
JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes
JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::GlobalObject*, JSC::EncodedJSValue stream, JSC::EncodedJSValue contentType); // userJS: yes
JSC::EncodedJSValue ZigGlobalObject__readableStreamConsumeThenReject(Zig::GlobalObject*, JSC::EncodedJSValue stream, JSC::EncodedJSValue error); // userJS: yes

} // extern "C"
79 changes: 45 additions & 34 deletions src/runtime/webcore/Body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,18 +421,23 @@ impl PendingValue {
Action::GetBytes => global_this.readable_stream_to_bytes(readable.value),
Action::GetText => global_this.readable_stream_to_text(readable.value),
Action::GetBlob => global_this.readable_stream_to_blob(readable.value),
Action::GetFormData(form_data) => 'brk: {
let fd = form_data.take().unwrap();
let encoding_js = match &fd.encoding {
bun_core::form_data::Encoding::Multipart(multipart) => {
bun_string_jsc::create_utf8_for_js(global_this, multipart)?
}
bun_core::form_data::Encoding::URLEncoded => JSValue::UNDEFINED,
};
// fd dropped at end of scope (Box<AsyncFormData> -> Drop)
break 'brk global_this
.readable_stream_to_form_data(readable.value, encoding_js);
}
Action::GetFormData(form_data) => match form_data.take() {
Some(fd) => {
let encoding_js = match &fd.encoding {
bun_core::form_data::Encoding::Multipart(multipart) => {
bun_string_jsc::create_utf8_for_js(global_this, multipart)?
}
bun_core::form_data::Encoding::URLEncoded => JSValue::UNDEFINED,
};
// fd dropped at end of scope (Box<AsyncFormData> -> Drop)
global_this
.readable_stream_to_form_data(readable.value, encoding_js)
}
None => global_this.readable_stream_consume_then_reject(
readable.value,
form_data_mime_error(global_this),
),
},
_ => unreachable!(),
};
self.readable.deinit();
Expand Down Expand Up @@ -473,6 +478,7 @@ pub(crate) enum Action {
GetArrayBuffer,
GetBytes,
GetBlob,
/// `None`: the Content-Type is not form data, so the read rejects once the body has been read.
GetFormData(Option<Box<bun_core::form_data::AsyncFormData>>),
}

Expand Down Expand Up @@ -1147,12 +1153,7 @@ impl Value {
let mut blob = new.use_as_any_blob();
let Some(async_form_data) = form_data_slot.take() else {
// `blob.detach()` below covers the reject error path too.
let r = promise.reject(
cx.global(),
cx.global().create_error_instance(format_args!(
"Internal error: task for FormData must not be null"
)),
);
let r = promise.reject(cx.global(), form_data_mime_error(cx.global()));
blob.detach();
r?;
break 'inner;
Expand Down Expand Up @@ -2088,17 +2089,8 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized {
}
}

let Some(encoder) = self.get_form_data_encoding()? else {
// TODO: catch specific errors from getFormDataEncoding
return Ok(global_object
.err(
jsc::ErrorCode::FORMDATA_PARSE_ERROR,
format_args!(
"Can't decode form data from body because of incorrect MIME type/boundary"
),
)
.reject());
};
// <https://fetch.spec.whatwg.org/#concept-body-consume-body> reads the body first, then a Content-Type that is not form data rejects.
let encoder = self.get_form_data_encoding()?;

let value = self.get_body_value();
if let Value::Locked(_locked) = value {
Expand All @@ -2108,13 +2100,20 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized {
let Value::Locked(locked) = value else {
unreachable!()
};
return locked.set_promise(
global_object,
Action::GetFormData(Some(encoder)),
owned_readable,
);
return locked.set_promise(global_object, Action::GetFormData(encoder), owned_readable);
}

let Some(encoder) = encoder else {
// Nothing to read in a null body.
if !matches!(value, Value::Null) {
*value = Value::Used;
}
return Ok(JSPromise::rejected_promise(
global_object,
form_data_mime_error(global_object),
)
.to_js());
};
let mut blob: AnyBlob = value.use_as_any_blob();
// `encoder.encoding` is `bun_core::form_data::Encoding`; convert
// to the `webcore::form_data::Encoding` shape FormData::to_js expects.
Expand Down Expand Up @@ -2235,6 +2234,18 @@ fn handle_body_already_used(global_object: &JSGlobalObject) -> JSValue {
.reject()
}

/// What `formData()` rejects with when the Content-Type is not form data.
fn form_data_mime_error(global_object: &JSGlobalObject) -> JSValue {
global_object
.err(
jsc::ErrorCode::FORMDATA_PARSE_ERROR,
format_args!(
"Can't decode form data from body because of incorrect MIME type/boundary"
),
)
.to_js()
}

/// <https://fetch.spec.whatwg.org/#body-unusable>: a disturbed or locked stream rejects every reader.
fn handle_body_stream_unusable(
readable: &ReadableStream,
Expand Down
Loading
Loading