Flush pending writes before closing the connection on caught exceptions#6177
Open
asalajan wants to merge 2 commits into
Open
Flush pending writes before closing the connection on caught exceptions#6177asalajan wants to merge 2 commits into
asalajan wants to merge 2 commits into
Conversation
e59ba09 to
a937b81
Compare
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>
a937b81 to
213479e
Compare
Member
|
for this fix we need a test in |
…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>
Author
I have added |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6155
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.
I'm happy to open the backport PRs for version 5.0.x and 5.1.x once this one is reviewed/merged
Developed with assistance from an AI coding agent (Claude Code). I reviewed and tested the changes.