Skip to content
Open
18 changes: 14 additions & 4 deletions src/jsc/JSGlobalObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1144,8 +1144,15 @@ impl JSGlobalObject {
crate::call_zero_is_throw(self, || ZigGlobalObject__readableStreamToJSON(self, value))
}

pub fn readable_stream_to_blob(&self, value: JSValue) -> JsResult<JSValue> {
crate::call_zero_is_throw(self, || ZigGlobalObject__readableStreamToBlob(self, value))
/// `content_type`: a JS string for the resulting Blob's `type`, or `undefined`.
pub fn readable_stream_to_blob(
&self,
value: JSValue,
content_type: JSValue,
) -> JsResult<JSValue> {
crate::call_zero_is_throw(self, || {
ZigGlobalObject__readableStreamToBlob(self, value, content_type)
})
}

pub fn readable_stream_to_form_data(
Expand Down Expand Up @@ -1565,8 +1572,11 @@ unsafe extern "C" {
value: JSValue,
content_type: JSValue,
) -> JSValue;
safe fn ZigGlobalObject__readableStreamToBlob(this: &JSGlobalObject, value: JSValue)
-> JSValue;
safe fn ZigGlobalObject__readableStreamToBlob(
this: &JSGlobalObject,
value: JSValue,
content_type: JSValue,
) -> JSValue;

safe fn ZigGlobalObject__makeNapiEnvForFFI(this: &JSGlobalObject) -> *mut c_void;

Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,6 @@ extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToText(Zig::Global
extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue);
extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBytes(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue);
extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue);
extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue);
extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue, JSC::EncodedJSValue contentType);

#endif
50 changes: 45 additions & 5 deletions src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
#include <wtf/Vector.h>
#include <wtf/text/StringBuilder.h>

// Blob.rs: types a JSBlob with a body owner's Content-Type header value.
extern "C" void Blob__setContentTypeFromHeader(JSC::EncodedJSValue blob, const BunString* contentType);

namespace WebCore {

using namespace JSC;
Expand Down Expand Up @@ -1260,28 +1263,40 @@ JSValue readableStreamToJSON(JSGlobalObject* globalObject, WebCore::JSReadableSt
return derived;
}

JSValue readableStreamToBlob(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream)
JSValue readableStreamToBlob(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, JSValue contentType)
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
if (isReadableStreamLocked(stream))
RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject)));
if (stream->m_disturbed)
RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject)));
if (!contentType || !contentType.isString())
contentType = jsUndefined();
auto* runtime = JSStreamsRuntime::from(globalObject);
JSValue fastPath = tryUseReadableStreamBufferedFastPath(globalObject, stream, builtinNames(vm).blobPublicName());
RETURN_IF_EXCEPTION(scope, {});
if (fastPath)
return fastPath;
if (fastPath) {
if (contentType.isUndefined())
return fastPath;
auto* blobPromise = dynamicDowncast<JSPromise>(fastPath);
if (!blobPromise) [[unlikely]] {
blobPromise = promiseFulfilledWith(globalObject, fastPath);
RETURN_IF_EXCEPTION(scope, {});
}
auto* derived = JSPromise::create(vm, globalObject->promiseStructure());
blobPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToBlobSetContentType(), jsUndefined(), derived, contentType);
return derived;
}
JSValue arrayResult = readableStreamToArray(globalObject, stream);
RETURN_IF_EXCEPTION(scope, {});
auto* arrayPromise = dynamicDowncast<JSPromise>(arrayResult);
if (!arrayPromise) [[unlikely]] {
arrayPromise = promiseFulfilledWith(globalObject, arrayResult);
RETURN_IF_EXCEPTION(scope, {});
}
auto* runtime = JSStreamsRuntime::from(globalObject);
auto* derived = JSPromise::create(vm, globalObject->promiseStructure());
arrayPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToBlobFulfilled(), jsUndefined(), derived, jsUndefined());
arrayPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToBlobFulfilled(), jsUndefined(), derived, contentType);
return derived;
}

Expand Down Expand Up @@ -1477,6 +1492,19 @@ JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToJSONFulfilled, (J
RELEASE_AND_RETURN(scope, JSValue::encode(JSONParseWithException(globalObject, text)));
}

// contentType is the JSString readableStreamToBlob() was given, or undefined.
static void setBlobContentTypeFromHeader(JSGlobalObject* globalObject, JSValue blob, JSValue contentType)
{
if (!contentType || !contentType.isString())
return;
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
WTF::String value = contentType.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, );
BunString header = Bun::toString(value);
Blob__setContentTypeFromHeader(JSValue::encode(blob), &header);
}

JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToBlobFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = getVM(globalObject);
Expand All @@ -1487,6 +1515,18 @@ JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToBlobFulfilled, (J
RETURN_IF_EXCEPTION(scope, {});
Bun::WebStreams::releaseInternalChunkArray(globalObject, callFrame->argument(0));
RETURN_IF_EXCEPTION(scope, {});
setBlobContentTypeFromHeader(globalObject, blob, callFrame->argument(1));
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(blob);
}

JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToBlobSetContentType, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue blob = callFrame->argument(0);
setBlobContentTypeFromHeader(globalObject, blob, callFrame->argument(1));
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(blob);
}

Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/webcore/streams/JSStreamsRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ namespace WebCore {
// onBufferedFastPath*: context = the JSReadableStream (the fast path's catch/finally pair).
// onReadableStreamTo*Fulfilled: the generic-path promise chains
// (toArrayBuffer/toBytes/toBlob: value = the chunk array; toJSON: value = the text;
// toBlob: context = the contentType JSString or undefined;
// toFormData: value = the Blob, context = the contentType JSString).
// onReadableStreamToBlobSetContentType: value = the fast path's Blob, context = the contentType JSString.
// 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 @@ -214,6 +216,7 @@ namespace WebCore {
V(onReadableStreamToTextChunksFulfilled) \
V(onReadableStreamToJSONFulfilled) \
V(onReadableStreamToBlobFulfilled) \
V(onReadableStreamToBlobSetContentType) \
V(onReadableStreamToFormDataFulfilled) \
V(onIntoArrayReadManyFulfilled) /* append value; !done => readMany() again; done => release + resolve */ \
V(onIntoArrayReadManyRejected) /* release the reader, reject the result promise */ \
Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/webcore/streams/WebStreamsExports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,13 @@ extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToJSON(Zig::Global
RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToJSON(globalObject, stream)));
}

extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue)
extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue, JSC::EncodedJSValue contentType)
{
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(readableStreamToBlob(globalObject, stream)));
RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToBlob(globalObject, stream, JSValue::decode(contentType))));
}

extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue, JSC::EncodedJSValue contentType)
Expand Down
5 changes: 3 additions & 2 deletions src/jsc/bindings/webcore/streams/WebStreamsInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,8 @@ JSC::JSValue readableStreamToArray(JSC::JSGlobalObject*, JSReadableStream*); //
JSC::JSValue readableStreamToArrayBuffer(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp
JSC::JSValue readableStreamToBytes(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp
JSC::JSValue readableStreamToJSON(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp
JSC::JSValue readableStreamToBlob(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp
// contentType: a JSString for the resulting Blob's type (Body.rs content_type_from_headers), or undefined.
JSC::JSValue readableStreamToBlob(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue contentType = JSC::jsUndefined()); // userJS: yes — BunStreamConsumers.cpp
JSC::JSValue readableStreamToFormData(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue contentType); // userJS: yes — BunStreamConsumers.cpp

// The buffered fast path: returns the native handle's own .text()/.arrayBuffer()/... promise,
Expand Down Expand Up @@ -732,7 +733,7 @@ JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObje
JSC::EncodedJSValue ZigGlobalObject__readableStreamToBytes(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes
JSC::EncodedJSValue ZigGlobalObject__readableStreamToText(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes
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__readableStreamToBlob(Zig::GlobalObject*, JSC::EncodedJSValue stream, JSC::EncodedJSValue contentType); // userJS: yes
JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::GlobalObject*, JSC::EncodedJSValue stream, JSC::EncodedJSValue contentType); // userJS: yes

} // extern "C"
5 changes: 2 additions & 3 deletions src/jsc/webcore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,10 @@ impl Blob {
// pulling the whole `BlobExt` trait into scope.
// ────────────────────────────────────────────────────────────────────

/// `Blob.hasContentTypeFromUser()` — `true` when the user set a type
/// explicitly *or* the store is file/S3-backed (whose mime is sniffed).
/// `Blob.hasContentTypeFromUser()`: a non-empty user-set type, or a file/S3 store (sniffed mime).
#[inline]
pub fn has_content_type_from_user(&self) -> bool {
self.content_type_was_set.get()
(self.content_type_was_set.get() && !self.content_type_slice().is_empty())
|| self
.store()
.map(|s| matches!(s.data, store::Data::File(_) | store::Data::S3(_)))
Expand Down
4 changes: 1 addition & 3 deletions src/runtime/api/html_rewriter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,8 +1554,6 @@ impl RewriterPipe {
let Some(response) = self.response.get().as_deref() else {
return;
};
// For a waiting `.blob()`'s content type.
let headers = response.get_fetch_headers().map(NonNull::from);
let body_value = response.get_body_value();
let bytes = self.output_buffer.replace(Vec::new());
let mut prev_value = core::mem::replace(
Expand All @@ -1565,7 +1563,7 @@ impl RewriterPipe {
was_string: false,
}),
);
let _ = webcore::body::Value::resolve(&mut prev_value, body_value, &self.global, headers);
let _ = webcore::body::Value::resolve(&mut prev_value, body_value, &self.global);
}

/// Feed the accumulated `pending_input` once unblocked, then maybe end,
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/server/RequestContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4247,7 +4247,7 @@ where
if matches!(old, Body::Value::Locked(_)) {
let _exit = vm.enter_event_loop_scope();

let _ = Body::Value::resolve(&mut old, body, global_this, None); // TODO: properly propagate exception upwards
let _ = Body::Value::resolve(&mut old, body, global_this); // an exception is not propagated upwards from here yet
}
return;
}
Expand Down Expand Up @@ -4406,7 +4406,7 @@ where
}
let mut new_body: Body::Value = Body::Value::Null;
let global_this = server.global_this();
let _ = Body::Value::resolve(&mut old, &mut new_body, global_this, None); // TODO: properly propagate exception upwards
let _ = Body::Value::resolve(&mut old, &mut new_body, global_this); // an exception is not propagated upwards from here yet
*body = new_body;
}
}
Expand Down
24 changes: 22 additions & 2 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1987,8 +1987,11 @@ impl BlobExt for Blob {
if !ct.is_empty() {
return EncodedSlice::latin1(ct).to_js(global_this);
}
if let Some(store) = self.store.get() {
return EncodedSlice::latin1(&store.mime_type.value).to_js(global_this);
// A set-but-empty type (a typed blob's `slice()`, an empty `Content-Type`) stays `""`.
if !self.content_type_was_set.get() {
if let Some(store) = self.store.get() {
return EncodedSlice::latin1(&store.mime_type.value).to_js(global_this);
}
}
JSValue::js_empty_string(global_this)
}
Expand Down Expand Up @@ -5888,6 +5891,23 @@ pub(crate) unsafe extern "C" fn Blob__fromBytesWithType(
blob
}

/// C++ `readableStreamToBlob` types the Blob it built with the body owner's `Content-Type`.
#[unsafe(no_mangle)]
pub(crate) extern "C" fn Blob__setContentTypeFromHeader(value: JSValue, content_type: &BunString) {
let Some(blob) = Blob::from_js(value) else {
return;
};
// SAFETY: `from_js` returns a non-null pointer to a live JSC-owned Blob.
let blob = unsafe { &*blob };
let content_type = content_type.to_utf8();
blob.content_type_was_set.set(true);
blob.content_type.set(if content_type.is_empty() {
BlobContentType::default()
} else {
BlobContentType::Owned(std::sync::Arc::from(&*content_type))
});
}

/// Adopts an mmap'd region — no copy. The Blob's store holds the mapping;
/// when the store's refcount drops to zero, deinit calls allocator.free
/// which munmap's.
Expand Down
Loading
Loading