Skip to content

Flush pending writes before closing the connection on caught exceptions#6226

Merged
vietj merged 6 commits into
5.1from
exception-caught-flush-before-close-5.1
Jul 6, 2026
Merged

Flush pending writes before closing the connection on caught exceptions#6226
vietj merged 6 commits into
5.1from
exception-caught-flush-before-close-5.1

Conversation

@vietj

@vietj vietj commented Jul 6, 2026

Copy link
Copy Markdown
Member
  • Flush pending writes before closing the connection on caught exceptions
  • Add a VertxConnectionTest reproducing the flush before close on exception
  • Move the flush-before-close-on-exception regression test to NetTest
  • Always close through the channel in VertxHandler.exceptionCaught
  • Remove comment
  • Skip the WebSocket closing handshake timeout on exception

asalajan added 6 commits July 6, 2026 10:41
Motivation:

When an exception is caught on a connection, VertxHandler#exceptionCaught
dispatches the throwable to the user handlers and then closes the channel
with chctx.close(). A response written from the handler -- for instance a
400 written from request().exceptionHandler() when HttpContentDecompressor
throws a DecoderException on a malformed compressed body -- is enqueued in
the outbound message queue but not flushed, because VertxConnection#write
skips the flush while a read is in progress. chctx.close() then causes Netty
to discard the unflushed entries from the ChannelOutboundBuffer, so the
response never reaches the client: the client observes a connection reset
instead of the intended HTTP response.

Changes:

Route the exception-driven close through the channel so it traverses the
pipeline tail and invokes VertxHandler#close -> VertxConnection#writeClose,
which flushes pending writes (an empty buffer with forceFlush=true) and
closes the channel only once the flush has completed. This is the same
flush-then-close path already used by graceful shutdown, idle close and the
regular close(); the exception path simply was not using it. The bare
chctx.close() is kept as a fallback for the case where no connection has been
set yet (handlerAdded has not run), since VertxHandler#close dereferences it.

Add a regression test, Http1xTest#testRequestDecompressionInvalidBodyDeliversErrorResponse,
that sends a malformed gzip body to a server with decompression enabled and
asserts the client receives the response written from the request exception
handler.

HTTP/2 is unaffected: its codec replaces VertxHandler in the pipeline, so
connection-level exceptions do not go through this path.

Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
…tion

Covers the fix at the connection layer: a write queued (unflushed) when a caught exception triggers the close must reach the channel before it closes, otherwise Netty's ChannelOutboundBuffer discards the unflushed entry. The test fails without the VertxHandler.exceptionCaught change and passes with it.

Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
The fix is at the transport layer (VertxHandler/VertxConnection) and is not HTTP specific, so exercise it over plain TCP rather than HTTP. Drop Http1xTest#testRequestDecompressionInvalidBodyDeliversErrorResponse and add NetTest#testFlushPendingWriteOnExceptionClose, which parks an unflushed write on a NetServer connection, triggers a caught exception that closes it, and asserts the client still receives the bytes before the close.

Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
A regular connection close already closes at the channel level, so the exception-driven close should too; there is no good reason to close from the channel handler context. Drop the connection != null / chctx.close() fallback and always use chctx.channel().close().

Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
When an exception aborts the connection there is no point waiting for the peer's close frame, so set the closing timeout to 0 in handleException.

Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
@vietj vietj added this to the 5.1.4 milestone Jul 6, 2026
@vietj vietj added the bug label Jul 6, 2026
@vietj vietj merged commit 4b16da6 into 5.1 Jul 6, 2026
9 of 15 checks passed
@vietj vietj deleted the exception-caught-flush-before-close-5.1 branch July 6, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants