fix(streams): mark internal rejections handled without author catch hooks

Use the native Promise handled flag for internal Streams promises instead of reading author-controlled catch methods. Cover locked reader release and writer abort with a poisoned Promise.prototype.catch getter.
This commit is contained in:
ldm0
2026-10-11 18:06:50 +08:00
parent b34d515d88
commit a6bb958b1f
3 changed files with 40 additions and 12 deletions
@@ -269,22 +269,16 @@ pub(in crate::context_bootstrap) fn suppress_pending_read_unhandled_rejection<'s
}
pub(in crate::context_bootstrap) fn suppress_promise_unhandled_rejection<'s>(
scope: &mut v8::PinScope<'s, '_>,
_: &mut v8::PinScope<'s, '_>,
promise: v8::Local<'s, v8::Value>,
) {
let Ok(promise) = v8::Local::<v8::Promise>::try_from(promise) else {
return;
};
let Some(noop) = v8::Function::builder(promise_return_undefined_callback).build(scope) else {
return;
};
let Some(catch) = promise
.get(scope, v8str(scope, "catch").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
else {
return;
};
let _ = catch.call(scope, promise.into(), &[noop.into()]);
// Streams algorithms mark their internal promises handled directly. An
// author can replace catch, its getter, or the Promise species constructor.
// Looking them up here would add observable script to the native algorithm.
promise.mark_as_handled();
}
pub(in crate::context_bootstrap) fn value_buffer_source_bytes(
@@ -1,5 +1,39 @@
use super::*;
#[test]
fn stream_internal_rejection_handling_does_not_read_author_promise_catch() {
let mut vm = stream_test_vm();
vm.eval(r#"
globalThis.__handlingResult='pending';
(async()=>{
let hooks=0;
const saved=Object.getOwnPropertyDescriptor(Promise.prototype,'catch');
Object.defineProperty(Promise.prototype,'catch',{configurable:true,get(){hooks++;throw Error('author catch getter');}});
try {
const reader=new ReadableStream().getReader();
const pending=reader.read().then(()=>false,error=>error instanceof TypeError);
const readerClosed=reader.closed.then(()=>false,error=>error instanceof TypeError);
reader.releaseLock();
const writer=new WritableStream().getWriter();
const writerClosed=writer.closed.then(()=>false,error=>error==='stop');
await writer.abort('stop');
const writerReady=writer.ready.then(()=>false,error=>error==='stop');
const results=await Promise.all([pending,readerClosed,writerClosed,writerReady]);
writer.releaseLock();
return JSON.stringify({hooks,results});
} finally {Object.defineProperty(Promise.prototype,'catch',saved);}
})().then(value=>globalThis.__handlingResult=value,error=>globalThis.__handlingResult='error:'+error.name+':'+error.message);
"#).unwrap();
let mut result = String::new();
for _ in 0..16 {
result = vm.eval("__handlingResult").unwrap();
if result != "pending" {
break;
}
}
assert_eq!(result, r#"{"hooks":0,"results":[true,true,true,true]}"#);
}
#[test]
fn writable_stream_writer_promises_survive_missing_or_poisoned_global_promise() {
for (label, replacement) in [
@@ -134,7 +134,7 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[
"context_bootstrap/stream_adapter/utils.rs",
// Pending promise settlement and cancellation continuations use the
// native Promise API; the remaining calls run stream algorithms.
5,
4,
DirectCallOwner::BrowserAlgorithm,
),
allowed(