James Netherton created CAMEL-24864:
---------------------------------------
Summary: camel-platform-http-vertx: response write failures are
discarded, leaking exchanges and SSE subscribers when a client disconnects
Key: CAMEL-24864
URL: https://issues.apache.org/jira/browse/CAMEL-24864
Project: Camel
Issue Type: Bug
Components: camel-platform-http-vertx
Reporter: James Netherton
Assignee: James Netherton
Fix For: 4.23.0
h3. Problem
VertxPlatformHttpSupport writes an InputStream response body by piping an
AsyncInputStream to the HttpServerResponse. This used the deprecated Pump, which
calls writeStream.write(data) and discards the result, so every response write
failure was invisible: the promise returned by writeResponse() completed
successfully even when nothing reached the client.
The consequence is that a client going away part way through a streaming
response
is never noticed by the consumer:
* The write failure is swallowed, so the pipe is not terminated.
* AsyncInputStream.close() is never called.
* The exchange never completes (no doneUoW / releaseExchange).
This is most visible for Server-Sent Events. A2AConsumer.setSseResponse puts an
SseQueueInputStream on the message with content type text/event-stream. That
stream only reaches EOF on an explicit end-of-stream marker from a terminal task
state; otherwise it emits heartbeats indefinitely. Its close() is what triggers
cleanup, via closeSubscriptionOnStreamClose -> setOnClose(...), which removes
the
subscriber. Because close() never runs when the client disconnects, the
subscriber is never removed and the exchange never completes, so both leak for
the lifetime of the application.
h3. Reproduction
Expose a platform-http endpoint whose body is an InputStream that keeps
producing
data (an SSE heartbeat stream, or any large download). Connect a client, read a
few KB, then abort the connection. Observed behaviour: the stream's close() is
never called, and the exchange stays open. Measured with a queue-backed
heartbeat
stream modelled on SseQueueInputStream: close() was not called within 15s of the
client disconnecting.
h3. Fix
Replace Pump with pipe(), which propagates write failures to the completion
promise, so the response write failure now reaches
VertxPlatformHttpConsumer.handleFailure, the stream is closed and the exchange
completes.
Propagating the failure also makes a normal client disconnect look like an
error,
which it is not, so two follow-on problems are fixed as part of this:
* A lost client connection must not be reported through the consumer's
ExceptionHandler (WARN plus stack trace) and must not fail the routing context
once the response head has been written. Note that ctx.response().closed() is
not sufficient on its own to detect this: Netty fails the write promise before
it reports the connection close to Vert.x, so the response is often not yet
flagged as closed when the failure is handled. Failures are therefore
classified by origin - a failure raised while reading the body stream is a
real
error, any other pipe failure is a failed response write.
* AsyncInputStream threw IllegalStateException from its handler and read methods
once closed, and logged "Unhandled error while processing stream" at ERROR for
the in-flight read that fails when the channel is gone. Both are expected
after
a disconnect and should not be reported as errors.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)