Skip to content

Conversation

sjy982
Copy link
Contributor

@sjy982 sjy982 commented Aug 17, 2025

Motivation

  • Instead of asking each handler to wrap the inbound stream with StreamMessage.timeout(..., UNTIL_NEXT), expose it as an option on WebSocketServiceBuilder / WebSocketClientBuilder so both server and client can enable it consistently and easily.
  • When the inbound completes abnormally (cancel/abort/timeout, etc.), further outbound sends are meaningless. We should immediately tidy up outbound and the stream/channel. If the connection is still valid and the termination is initiated locally, prefer sending a WebSocket close frame (with an appropriate status + reason) over a transport reset, for better reason propagation, interoperability, and observability.

Modifications

  • Add WebSocketServiceBuilder#streamTimeout(Duration).

    • When set, wrap inbound in TimeoutStreamMessage with StreamTimeoutMode.UNTIL_NEXT inside DefaultWebSocketService#serve(...).
  • Update DefaultWebSocketService#serve(...):

    • If inbound completes with an error, call outbound.abort(mappedCause) so recoverAndResume sends a CloseWebSocketFrame.
    • Map CancelledSubscriptionException / AbortedStreamException to InboundCompleteException to avoid being skipped by the recovery logic.
  • Add WebSocketClientBuilder#streamTimeout(Duration).

    • When set, wrap inbound in TimeoutStreamMessage with StreamTimeoutMode.UNTIL_NEXT inside DefaultWebSocketClient#connect(...).
  • Update WebSocketSession#setOutbound(...):

    • Attach recoverAndResume(...) to outbound so that on send failure it emits a CloseWebSocketFrame and records the exception in RequestLog (endRequest(cause) / endResponse(cause)). For ClosedStreamException, do not send a close frame—just abort.
    • When inbound completes exceptionally, abort(...) outbound with the mapped cause so the recovery stream kicks in.
  • Add InboundCompleteException extends CancellationException:

    • Signals that inbound completed due to cancellation/abort.
  • Add a client-side newCloseWebSocketFrame(Throwable) in WebSocketUtil.

Result

  • WebSocketService / WebSocketClient can apply StreamMessage.timeout(..., UNTIL_NEXT) to inbound via the simple streamTimeout(Duration) option.
  • When inbound completes with an error:
    • If the channel is still usable, abort outbound with the mapped cause so a close frame is sent (skip sending for ClosedStreamException).
    • Record request/response causes in RequestLog.
    • On HTTP/1.x, close the channel; on HTTP/2, clean up the stream.

sjy982 added 7 commits August 11, 2025 18:09
…uestLog, ensure close frame on cancel/abort; add tests (H2C close-frame/log, H1 channel close)
…utbound, closed outbound when inbound ends, introduced InboundCompleteException, and added a client-side newCloseWebSocketFrame() in WebSocketUtil.
Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, left some comments

Copy link

codecov bot commented Aug 28, 2025

Codecov Report

❌ Patch coverage is 80.45977% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.14%. Comparing base (8150425) to head (1de6137).
⚠️ Report is 183 commits behind head on main.

Files with missing lines Patch % Lines
...rp/armeria/common/stream/TimeoutStreamMessage.java 73.68% 5 Missing ⚠️
...orp/armeria/client/websocket/WebSocketSession.java 75.00% 2 Missing and 2 partials ⚠️
...corp/armeria/common/logging/DefaultRequestLog.java 82.35% 1 Missing and 2 partials ⚠️
...ecorp/armeria/common/InboundCompleteException.java 50.00% 2 Missing ⚠️
...meria/client/websocket/WebSocketClientBuilder.java 80.00% 0 Missing and 1 partial ⚠️
.../linecorp/armeria/common/stream/StreamMessage.java 0.00% 0 Missing and 1 partial ⚠️
...eria/server/websocket/WebSocketServiceBuilder.java 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6357      +/-   ##
============================================
- Coverage     74.46%   74.14%   -0.33%     
- Complexity    22234    23022     +788     
============================================
  Files          1963     2062      +99     
  Lines         82437    86177    +3740     
  Branches      10764    11317     +553     
============================================
+ Hits          61385    63892    +2507     
- Misses        15918    16872     +954     
- Partials       5134     5413     +279     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

fix: In WebSocketSession endRequest -> requestCause
@sjy982 sjy982 requested a review from ikhoon September 2, 2025 10:47
jrhee17 added a commit that referenced this pull request Sep 16, 2025
… frame (#6375)

Motivation:

Currently, once an `HttpRequest` and `HttpResponse` is completed, the
underlying HTTP2 stream is cancelled using a `RST_STREAM`.
This makes sense for normal HTTP constructs since it indicates that we
are no longer interested in the request, and we would like to release
resources associated with it.

However, some protocols such as WebSockets implement their own graceful
shutdown procedure.

In detail, Armeria's `HttpRequest`, `HttpResponse` implements WebSocket
graceful shutdown and the reactive stream implementation is closed when
a `CLOSE` frame is both sent and received.
However, although the websocket session is completed, the underlying
HTTP2 stream may not necessarily be complete.
Protocol-wise, there is an inherent discrepancy between websocket
session completion and HTTP2 stream completion.

The current implementation defaults to sending a `RST_STREAM`
immediately once the corresponding `HttpRequest` and `HttpResponse`. For
websockets, I propose that a delay is given so that the remote has a
chance to end the stream.

Assuming that we will tie the lifecycle of inbound `WebSocket`s with
outbound `WebSocket`s (so sending a close frame also closes the inbound)
in #6357 , this option can be thought of similar to netty's
`forceCloseTimeoutMillis` option. (which acts as a timeout since sending
the CLOSE frame)

Modifications:

- Added a `closeHttp2StreamDelayMillis` option to `ServiceConfig` and
relevant implementations
- `WebSocketService` sets a `closeHttp2StreamDelayMillis` of 10 seconds
by default
- `HttpServerHandler` decides when to send a `RST_STREAM` based on the
`closeHttp2StreamDelayMillis`
- Added `Http2StreamLifecycleHandler` which maintains the lifecycle of
reset futures. This ensures that scheduled futures aren't leaked for
servers with high throughput.
- Every time a request/response is closed but the corresponding stream
is alive, `maybeResetStream` is called.
- Every time a stream is closed, `notifyStreamClosed` is called to clean
up possibly scheduled futures.

Result:

- Users can set a timeout for closing a websocket session
- Fix a bug where closing a websocket session could send a `RST_STREAM`
frame

<!--
Visit this URL to learn more about how to write a pull request
description:

https://armeria.dev/community/developer-guide#how-to-write-pull-request-description
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants