Skip to content

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

Open
asalajan wants to merge 5 commits into
eclipse-vertx:masterfrom
asalajan:exception-caught-flush-before-close
Open

Flush pending writes before closing the connection on caught exceptions#6177
asalajan wants to merge 5 commits into
eclipse-vertx:masterfrom
asalajan:exception-caught-flush-before-close

Conversation

@asalajan

Copy link
Copy Markdown

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.

@asalajan asalajan force-pushed the exception-caught-flush-before-close branch 2 times, most recently from a937b81 to 213479e Compare June 10, 2026 11:25
@vietj

vietj commented Jun 10, 2026

Copy link
Copy Markdown
Member

for this fix we need a test in VertxConnectionTest that reproduces the situation as well.

@asalajan

Copy link
Copy Markdown
Author

for this fix we need a test in VertxConnectionTest that reproduces the situation as well.

I have added testFlushPendingWriteOnExceptionClose() in VertxConnectionTest

@asalajan

Copy link
Copy Markdown
Author

Hello @vietj !
Is there anything that still needs addressing for this PR ?

@vietj vietj added this to the 5.2.0 milestone Jun 23, 2026
@vietj

vietj commented Jun 23, 2026

Copy link
Copy Markdown
Member

@asalajan as I understand we are testing the behavior using the NetClient communication to the HTTP server ?

@asalajan

asalajan commented Jun 23, 2026

Copy link
Copy Markdown
Author

@vietj Yes, for testRequestDecompressionInvalidBodyDeliversErrorResponse, raw NetClient is used to capture the response bytes directly and assert the 400 reaches the wire before the connection closes which is the exact behaviour the fix restores.
A HttpClient version would work too, but NetClient gives a direct signal (empty buffer vs. the full HTTP/1.1 400) without going through the client HTTP stack.

For the second test, testFlushPendingWriteOnExceptionClose, EmbeddedChannel is used to isolate the change from the HTTP/decompression specifics

@asalajan

Copy link
Copy Markdown
Author

@vietj I've edited my previous response to try to clarify why NetClient was used.

@vietj

vietj commented Jun 23, 2026

Copy link
Copy Markdown
Member

I would like actually this test to be in NetClientTest and avoid make it an HTTP test because it is not related to HTTP protocol

asalajan added 3 commits June 24, 2026 10:34
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>
@asalajan asalajan force-pushed the exception-caught-flush-before-close branch from eb45653 to 00b3174 Compare June 24, 2026 09:29
@asalajan

Copy link
Copy Markdown
Author

I would like actually this test to be in NetClientTest and avoid make it an HTTP test because it is not related to HTTP protocol

@vietj I moved the test. Please let me know if you see any other issues.

Comment thread vertx-core/src/main/java/io/vertx/core/net/impl/VertxHandler.java Outdated
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>
Comment thread vertx-core/src/main/java/io/vertx/core/net/impl/VertxHandler.java Outdated
Signed-off-by: Alexandru Salajan <alexandru17@gmail.com>
@asalajan

Copy link
Copy Markdown
Author

I see 3 WebSocket tests are failing because of this change. I missed running them locally:

Error:  Errors: 
Error:    WebSocketTest.testReportProtocolViolationOnClient » Timeout Unsatisfied checkpoint
Error:    WebSocketTest.testReportProtocolViolationOnServer » Timeout Unsatisfied checkpoint
Error:    WebSocketTest.testServerWebSocketExceptionHandlerIsCalled » Timeout Unsatisfied checkpoint
Error:  Tests run: 6770, Failures: 0, Errors: 3, Skipped: 142

They pass on master and fail with the fix with both the unconditional and the earlier guarded form - anything that reaches VertxHandler.close.

I am not familiar with WebSockets but I can try to look into it more with the help of AI.

Maybe one option could be to narrow the fix to the HTTP/1 layer by flushing the response in the server connection exception handling before close, and VertxHandler.exceptionCaught remains as it is: chctx.close(). I don't know if a generic protocol agnostic solution exists.

@vietj

vietj commented Jun 25, 2026

Copy link
Copy Markdown
Member

@asalajan I will have a look myself

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP/1.1 response written from an exception handler is discarded before reaching the client

2 participants