Skip to content

Commit 845fe1b

Browse files
tillrohrmannclaude
andcommitted
fix: skip empty output buffers to avoid empty HTTP/2 DATA frames
take_output returns an empty buffer when there is nothing to send but the output stream is not closed (e.g. while replaying already-completed entries on resume-after-suspend). The SDK forwarded this empty buffer to the response channel unconditionally, emitting one empty HTTP/2 DATA frame per replayed await. Proxies such as Envoy terminate connections with more than one consecutive empty-payload frame, so suspended invocations never resumed. Guard the three send sites (async_result_poll, select_poll, consume_to_end) to skip empty buffers. The EOF arm is unchanged, so a closed output still maps to UnexpectedOutputClosed. Fixes #114 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4e937e8 commit 845fe1b

3 files changed

Lines changed: 11 additions & 2 deletions

File tree

src/endpoint/context.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ impl ContextInternal {
757757

758758
let out = inner_lock.vm.take_output();
759759
if let TakeOutputResult::Buffer(b) = out
760+
&& !b.is_empty()
760761
&& !inner_lock.write.send(b)
761762
{
762763
// Nothing we can do anymore here

src/endpoint/futures/async_result_poll.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ impl Future for VmAsyncResultPollFuture {
5959
let out = inner_lock.vm.take_output();
6060
match out {
6161
TakeOutputResult::Buffer(b) => {
62-
if !inner_lock.write.send(b) {
62+
// Skip empty buffers: take_output returns an empty buffer when there's
63+
// nothing to send (e.g. while replaying completed entries). Sending it
64+
// would emit an empty HTTP/2 DATA frame per replayed await, which some
65+
// proxies (e.g. Envoy) reject when consecutive. See sdk-rust#114.
66+
if !b.is_empty() && !inner_lock.write.send(b) {
6367
return Poll::Ready(Err(ErrorInner::Suspended));
6468
}
6569
}

src/endpoint/futures/select_poll.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ impl Future for VmSelectAsyncResultPollFuture {
6060
let out = inner_lock.vm.take_output();
6161
match out {
6262
TakeOutputResult::Buffer(b) => {
63-
if !inner_lock.write.send(b) {
63+
// Skip empty buffers: take_output returns an empty buffer when there's
64+
// nothing to send (e.g. while replaying completed entries). Sending it
65+
// would emit an empty HTTP/2 DATA frame per replayed await, which some
66+
// proxies (e.g. Envoy) reject when consecutive. See sdk-rust#114.
67+
if !b.is_empty() && !inner_lock.write.send(b) {
6468
return Poll::Ready(Err(ErrorInner::Suspended));
6569
}
6670
}

0 commit comments

Comments
 (0)