diff --git a/crates/perry-ext-http-server/src/dispatch_ext.rs b/crates/perry-ext-http-server/src/dispatch_ext.rs index afcd854608..cb60837477 100644 --- a/crates/perry-ext-http-server/src/dispatch_ext.rs +++ b/crates/perry-ext-http-server/src/dispatch_ext.rs @@ -214,6 +214,7 @@ fn is_server_response_member(name: &str) -> bool { | "writableEnded" | "writableFinished" | "finished" + | "destroyed" | "writableCorked" | "writableHighWaterMark" | "writableLength" @@ -235,6 +236,7 @@ fn is_server_response_member(name: &str) -> bool { | "__get_writableEnded" | "__get_writableFinished" | "__get_finished" + | "__get_destroyed" | "__get_sendDate" | "__set_sendDate" | "__get_strictContentLength" diff --git a/crates/perry-ext-http-server/src/handle_dispatch.rs b/crates/perry-ext-http-server/src/handle_dispatch.rs index 851675a356..4ac6ed1fc7 100644 --- a/crates/perry-ext-http-server/src/handle_dispatch.rs +++ b/crates/perry-ext-http-server/src/handle_dispatch.rs @@ -22,7 +22,9 @@ //! //! Issue #2153. -use perry_ffi::{alloc_string, get_handle, js_object_alloc_with_shape, JsValue, StringHeader}; +use perry_ffi::{ + alloc_string, get_handle, get_handle_mut, js_object_alloc_with_shape, JsValue, StringHeader, +}; use crate::http2_server::Http2SecureServer; use crate::https_server::HttpsServer; @@ -788,7 +790,16 @@ pub unsafe extern "C" fn js_ext_http_server_response_dispatch_method( js_node_http_res_write_processing(handle); undef } - "destroy" => self_ref, + // #4975: `outgoingMessage.destroy()` flips the `destroyed` flag (read + // back by the `destroyed` getter) and returns `this`. A subsequent + // `write()` then errors its callback with `ERR_STREAM_DESTROYED` + // rather than buffering (see `js_node_http_res_write_with_cb`). + "destroy" => { + if let Some(sr) = get_handle_mut::(handle) { + sr.destroyed = true; + } + self_ref + } "assignSocket" if !args.is_empty() => { crate::response::js_node_http_res_assign_socket(handle, args[0]); undef @@ -834,6 +845,13 @@ pub unsafe extern "C" fn js_ext_http_server_response_dispatch_method( "__get_writableEnded" => bool_value(js_node_http_res_writable_ended(handle) != 0), "__get_writableFinished" => bool_value(js_node_http_res_writable_finished(handle) != 0), "__get_finished" | "finished" => bool_value(js_node_http_res_finished(handle) != 0), + // #4975: `outgoingMessage.destroyed` getter, also reachable through the + // `__get_destroyed` codegen form. + "__get_destroyed" | "destroyed" => bool_value( + get_handle::(handle) + .map(|sr| sr.destroyed) + .unwrap_or(false), + ), "__get_sendDate" | "sendDate" => bool_value(js_node_http_res_send_date(handle) != 0), "__set_sendDate" if !args.is_empty() => { js_node_http_res_set_send_date(handle, args[0]); @@ -952,6 +970,12 @@ pub unsafe extern "C" fn js_ext_http_server_response_dispatch_property( "writableEnded" => bool_value(js_node_http_res_writable_ended(handle) != 0), "writableFinished" => bool_value(js_node_http_res_writable_finished(handle) != 0), "finished" => bool_value(js_node_http_res_finished(handle) != 0), + // #4975: `outgoingMessage.destroyed` — false until `destroy()`. + "destroyed" => bool_value( + get_handle::(handle) + .map(|sr| sr.destroyed) + .unwrap_or(false), + ), "writableCorked" => 0.0, "writableHighWaterMark" => 65_536.0, "writableLength" => get_handle::(handle) diff --git a/crates/perry-ext-http-server/src/response.rs b/crates/perry-ext-http-server/src/response.rs index a810a1b43c..f737feff2c 100644 --- a/crates/perry-ext-http-server/src/response.rs +++ b/crates/perry-ext-http-server/src/response.rs @@ -255,6 +255,11 @@ pub struct ServerResponse { /// #4904: `res.write(chunk, cb)` callbacks, invoked in order when the /// buffered body flushes on `.end()`. pub pending_write_callbacks: Vec, + /// #4975: `outgoingMessage.destroy()` state. Node's `OutgoingMessage` + /// (and its `ServerResponse` subclass) exposes a `destroyed` getter that + /// flips `true` after `destroy()`, and a post-destroy `write(chunk, cb)` + /// invokes `cb` with an `ERR_STREAM_DESTROYED` error instead of buffering. + pub destroyed: bool, } /// Owned shape produced by `.end()` — the per-request oneshot channel @@ -412,6 +417,7 @@ impl ServerResponse { standalone_socket: f64::from_bits(TAG_UNDEFINED), standalone_req_method: None, pending_write_callbacks: Vec::new(), + destroyed: false, } } @@ -1748,6 +1754,23 @@ pub extern "C" fn js_node_http_res_detach_socket(handle: i64, _socket: f64) { /// flushes on `.end()`, preserving call order (#4904). #[no_mangle] pub extern "C" fn js_node_http_res_write_with_cb(handle: i64, chunk: f64, callback: i64) -> i32 { + // #4975: `write()` after `destroy()` must not buffer; Node invokes the + // callback with an `ERR_STREAM_DESTROYED` error and returns `false` (no + // `'error'` event is emitted, so an `on('error', …)` listener stays silent). + if get_handle::(handle) + .map(|sr| sr.destroyed) + .unwrap_or(false) + { + if callback != 0 { + let err = perry_ffi::error_value_with_code( + "Cannot call write after a stream was destroyed", + "ERR_STREAM_DESTROYED", + perry_ffi::ErrorKind::Error, + ); + crate::http2_server::call1(callback, f64::from_bits(err.bits())); + } + return 0; + } let bytes = jsvalue_to_body_bytes(chunk); // Honor streaming mode (after `res.flushHeaders()` / a prior streamed // `res.write`) exactly like `js_node_http_res_write`: the chunk must go down diff --git a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs index 1a8020918c..911b198b45 100644 --- a/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/method_dispatch.rs @@ -683,6 +683,7 @@ pub unsafe extern "C" fn js_handle_method_dispatch( | "__get_writableEnded" | "__get_writableFinished" | "__get_finished" + | "__get_destroyed" | "__get_sendDate" | "__set_sendDate" | "__get_strictContentLength" diff --git a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs index e546739eb9..4f3c5944d7 100644 --- a/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs +++ b/crates/perry-stdlib/src/common/dispatch/property_dispatch.rs @@ -344,6 +344,7 @@ pub unsafe extern "C" fn js_handle_property_dispatch( | "writableEnded" | "writableFinished" | "finished" + | "destroyed" | "writableCorked" | "writableHighWaterMark" | "writableLength"