From 426ee05e6947e9bbb5b3def5255b6e380a24a472 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Tue, 21 Apr 2026 16:11:40 -0300 Subject: [PATCH] fix(undertow): resolve buffer already freed exception during shutdown Fixes an `IllegalStateException: UT000091: Buffer has already been freed` that occurs during server teardown in gRPC tests. When raw request or response channels are acquired, Undertow shifts the exchange lifecycle management to the channels. Explicitly invoking `exchange.endExchange()` prematurely frees the pooled ByteBuffers. During server shutdown, the HTTP/2 `FrameCloseListener` attempts to gracefully close the connection and release the same buffers, resulting in a race condition and the subsequent exception. Removed explicit `exchange.endExchange()` calls when streams are active in `UndertowGrpcExchange` and `UndertowGrpcInputBridge`. Replaced them with XNIO's `IoUtils.safeClose(channel)`, allowing Undertow's internal state machine to naturally flush, close, and terminate the exchange. --- .../internal/undertow/UndertowGrpcExchange.java | 14 ++++++++++---- .../internal/undertow/UndertowGrpcInputBridge.java | 2 -- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcExchange.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcExchange.java index b2508f2511..f46f7dce69 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcExchange.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcExchange.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.function.Consumer; +import org.xnio.IoUtils; import org.xnio.channels.StreamSinkChannel; import io.jooby.rpc.grpc.GrpcExchange; @@ -144,19 +145,19 @@ public void close(int statusCode, String description) { try { if (ch.flush()) { ch.suspendWrites(); - exchange.endExchange(); + endExchange(); } } catch (IOException ignored) { ch.suspendWrites(); - exchange.endExchange(); + endExchange(); } }); responseChannel.resumeWrites(); } else { - exchange.endExchange(); + endExchange(); } } catch (IOException e) { - exchange.endExchange(); + endExchange(); } } else { @@ -170,4 +171,9 @@ public void close(int statusCode, String description) { exchange.endExchange(); } } + + private void endExchange() { + IoUtils.safeClose(responseChannel); + IoUtils.safeClose(exchange.getRequestChannel()); + } } diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcInputBridge.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcInputBridge.java index 84d05a0293..2d68b31899 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcInputBridge.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowGrpcInputBridge.java @@ -55,7 +55,6 @@ public void request(long n) { public void cancel() { demand.set(0); IoUtils.safeClose(channel); - exchange.endExchange(); } @Override @@ -90,7 +89,6 @@ public void handleEvent(StreamSourceChannel channel) { } catch (Throwable t) { subscriber.onError(t); IoUtils.safeClose(channel); - exchange.endExchange(); } } }