-
Notifications
You must be signed in to change notification settings - Fork 5k
Bun.serve: send the buffered tail when a direct stream controller is close()d #37696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1051,8 +1051,8 @@ pub struct HTTPServerWritable<const SSL: bool, const HTTP3: bool> { | |
| /// `flush_promise()` → `pending.run()`. | ||
| pub(crate) pending: WritablePending, | ||
| pub(crate) wrote_at_start_of_flush: BlobSizeType, | ||
| // JSC_BORROW: process-lifetime VM global; `None` until `flush_from_js`/ | ||
| // `end_from_js` install it. Safe `Deref` via `BackRef`. | ||
| // JSC_BORROW: process-lifetime VM global, installed by | ||
| // `RequestContext::do_render_stream` at construction. Safe `Deref` via `BackRef`. | ||
| pub global_this: Option<BackRef<JSGlobalObject>>, | ||
| pub(crate) high_water_mark: BlobSizeType, | ||
|
|
||
|
|
@@ -1643,13 +1643,7 @@ impl<const SSL: bool, const HTTP3: bool> HTTPServerWritable<SSL, HTTP3> { | |
| } | ||
| } | ||
| self.wrote_at_start_of_flush = self.wrote; | ||
| self.pending_flush = Some(JSPromise::create(global_this)); | ||
| self.global_this = Some(BackRef::new(global_this)); | ||
| // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. | ||
| let promise_value = JSPromise::opaque_ref(self.pending_flush.unwrap()).to_js(); | ||
| promise_value.protect(); | ||
|
|
||
| bun_sys::Result::Ok(promise_value) | ||
| bun_sys::Result::Ok(self.park_pending_flush(global_this)) | ||
| } | ||
|
|
||
| pub fn flush(&mut self) -> bun_sys::Result<()> { | ||
|
|
@@ -1806,7 +1800,9 @@ impl<const SSL: bool, const HTTP3: bool> HTTPServerWritable<SSL, HTTP3> { | |
| self.unregister_auto_flusher(); | ||
| } | ||
|
|
||
| /// In this case, it's always an error | ||
| /// Controller `close()`. The buffered tail is sent here rather than left | ||
| /// to the auto-flusher: a `pull()` that closes synchronously has its sink | ||
| /// torn down by `do_render_stream` before any deferred task runs. | ||
|
Comment on lines
+1803
to
+1805
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| pub(crate) fn end(&mut self, err: Option<SysError>) -> bun_sys::Result<()> { | ||
| bun_core::scoped_log!(HTTPServerWritableLog, "end({:?})", err); | ||
|
|
||
|
|
@@ -1833,6 +1829,13 @@ impl<const SSL: bool, const HTTP3: bool> HTTPServerWritable<SSL, HTTP3> { | |
| self.finalize(); | ||
| return bun_sys::Result::Ok(()); | ||
| } | ||
|
|
||
| if self.send_readable(0) { | ||
| self.handle_ended_response(err); | ||
| } else { | ||
| let global_this = BackRef::new(self.global_this()); | ||
| self.park_pending_flush(&global_this); | ||
| } | ||
| bun_sys::Result::Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -1857,32 +1860,46 @@ impl<const SSL: bool, const HTTP3: bool> HTTPServerWritable<SSL, HTTP3> { | |
|
|
||
| if readable_len > 0 { | ||
| if !self.send_readable(0) { | ||
| self.pending_flush = Some(JSPromise::create(global_this)); | ||
| self.global_this = Some(BackRef::new(global_this)); | ||
| // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. | ||
| let value = JSPromise::opaque_ref(self.pending_flush.unwrap()).to_js(); | ||
| value.protect(); | ||
| return bun_sys::Result::Ok(value); | ||
| return bun_sys::Result::Ok(self.park_pending_flush(global_this)); | ||
| } | ||
| } else { | ||
| if let Some(res) = self.any_res() { | ||
| res.end(b"", false); | ||
| } | ||
| } | ||
|
|
||
| self.handle_ended_response(None); | ||
|
|
||
| bun_sys::Result::Ok(JSValue::from(self.wrote)) | ||
| } | ||
|
|
||
| /// uWS could not drain what was handed to it: `on_writable` settles this | ||
| /// promise via `flush_promise` once it has (resending the `try_end` tail | ||
| /// still in `buffer` first). For an ended sink the request waits on it | ||
| /// instead of tearing the sink down (`do_render_stream`, | ||
| /// `handle_resolve_stream`). | ||
|
Comment on lines
+1876
to
+1880
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| fn park_pending_flush(&mut self, global_this: &JSGlobalObject) -> JSValue { | ||
| let promise = JSPromise::create(global_this); | ||
| self.pending_flush = Some(promise); | ||
| self.global_this = Some(BackRef::new(global_this)); | ||
| // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. | ||
| let value = JSPromise::opaque_ref(promise).to_js(); | ||
| value.protect(); | ||
| value | ||
| } | ||
|
|
||
| /// `end()`/`end_from_js()` fully ended the response through uWS, which | ||
| /// `markDone()`s it and drops its `onAborted` (see `ended_response`). | ||
|
Comment on lines
+1891
to
+1892
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| fn handle_ended_response(&mut self, err: Option<SysError>) { | ||
| if let Some(res) = self.any_res() { | ||
| // Release any request-body pause while `res` is live (see `end_already_responded_stream`). | ||
| res.resume(); | ||
| } | ||
| // Both branches above fully ended the response through uWS, which | ||
| // `markDone()`s it and drops its `onAborted`. | ||
| self.ended_response = true; | ||
| self.mark_done(); | ||
| let _ = self.flush_promise(); // TODO: properly propagate exception upwards | ||
| self.source.close(None); | ||
| let _ = self.flush_promise(); | ||
| self.source.close(err); | ||
| self.finalize(); | ||
|
|
||
| bun_sys::Result::Ok(JSValue::from(self.wrote)) | ||
| } | ||
|
|
||
| /// Takes `*mut Self`, not `&mut self`: closing the signal runs the controller's | ||
|
|
@@ -1942,6 +1959,9 @@ impl<const SSL: bool, const HTTP3: bool> HTTPServerWritable<SSL, HTTP3> { | |
| self.auto_flusher.registered.set(false); | ||
| return false; | ||
| } | ||
| // `end()`/`end_from_js()` send their own tail (or park it for | ||
| // `on_writable`) and unregister this flusher on the way. | ||
|
Comment on lines
+1962
to
+1963
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code |
||
| debug_assert!(!self.requested_end); | ||
|
|
||
| let readable_len = self.readable_slice().len(); | ||
|
|
||
|
|
@@ -1955,20 +1975,6 @@ impl<const SSL: bool, const HTTP3: bool> HTTPServerWritable<SSL, HTTP3> { | |
| return true; | ||
| } | ||
| self.auto_flusher.registered.set(false); | ||
|
|
||
| if self.requested_end { | ||
| if let Some(res) = self.any_res() { | ||
| res.clear_on_writable(); | ||
| // Release any request-body pause while `res` is live (see `end_already_responded_stream`). | ||
| res.resume(); | ||
| } | ||
| // `send_readable` drained the parked `try_end`/`end`, so uWS has | ||
| // `markDone()`d the response and dropped its `onAborted`. | ||
| self.ended_response = true; | ||
| self.source.close(None); | ||
| let _ = self.flush_promise(); | ||
| self.finalize(); | ||
| } | ||
| false | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code